Expand description
GraphQL awareness (DW-099): query depth/complexity limits and persisted-query enforcement for routes that front a GraphQL server.
§Design
GraphQL endpoints are uniquely abuse-prone: a single small request can express an arbitrarily deep or wide query that amplifies into expensive upstream work. This module provides a request-phase check that rejects abusive queries BEFORE any resource is spent on auth or rate limiting, exactly like the WAF-lite (DW-051) and anomaly (DW-090) phases it sits beside.
Two defenses, both feature-gated behind the graphql cargo feature:
-
Depth/complexity limits: a bounded hand-rolled scanner walks the query string counting nesting depth (max brace nesting) and field count (complexity). No external GraphQL parser is pulled in – the scanner only needs to count braces and field separators, not fully parse the query, which avoids any new dependency (and the deny.toml review it would require). The scanner is bounded: a parse-depth cap prevents deeply nested brace DoS against the scanner itself, and a body-size cap rejects oversized bodies before any parsing begins.
-
Persisted queries (Apollo APQ variant + GET-by-hash): when enabled, the gateway enforces that every request either carries a known query hash (the SHA-256 of the query text, sent as the
x-query-hashextension field or the?query_hash=query parameter) or is exempted by a config-supplied allowlist. A hash not in the configured store is rejected with 400graphql_persisted_query_required. The store is config-supplied (a map of hash -> query text); an external store is a future extension point.
§Request-path position
The GraphQL check runs AFTER the WAF-lite filter and anomaly scoring
and BEFORE the route limits (DW-027): it is a content-shape filter
that rejects abusive queries before any resource is spent on auth or
rate limiting. It inspects the ORIGINAL request body (before
transforms). Only routes with a graphql block AND the graphql
cargo feature compiled in are inspected; routes without the block are
never checked, and when the feature is off the block is accepted but
inert (the config schema is always present, the runtime check is
feature-gated).
§Cost model
Complexity is the sum of per-field costs. A config-supplied
cost_per_field map gives named fields explicit costs; any field not
in the map falls back to complexity_coefficient (default 1). The
total is sum(field_cost) where field_cost = cost_per_field[name]
if present, else complexity_coefficient. Depth is the maximum
brace-nesting level reached in the query (the top-level operation
body is depth 1).
Structs§
- GraphQL
Checker - A compiled GraphQL check for one route, built from the route’s
RouteGraphqlconfig block. Cheap to construct (clones the config values); held by the request path for the duration of one check. - Graphql
Body - A body that replays the bytes collected by the GraphQL check (so
the body is not consumed by the check and can be forwarded to the
upstream). This mirrors the WAF body-replay pattern (DW-051): the
check buffers up to
max_body_bytesand the reconstructed body is forwarded to the rest of the request path.
Enums§
- GraphQL
Body Error - Error returned when the body cannot be read for the GraphQL check.
- GraphQL
Check Result - The outcome of a GraphQL check.
Constants§
- DEFAULT_
GRAPHQL_ MAX_ BODY_ BYTES - Maximum body size the checker will buffer before parsing (default 1 MiB). Oversized bodies are rejected with 413 before any parsing, so a hostile client cannot pin memory or CPU with a huge payload.
- PARSE_
DEPTH_ CAP - Maximum brace-nesting depth the scanner will track before aborting
(parser-DoS cap). A query this deep is either hostile or malformed;
the scanner stops counting and the check fails closed (denied as
depth exceeded). This is NOT the user-configured
depth_limit– it is a hard internal bound on the scanner itself.
Functions§
- scan_
depth_ complexity - Scan a GraphQL query string for depth and complexity. This is a bounded hand-rolled scanner – it does NOT fully parse GraphQL. It counts:
- sha256_
hex - Compute the SHA-256 hash of a byte slice and return it as a lowercase hex string (the APQ hash format).