Module graphql

Module graphql 

Source
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:

  1. 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.

  2. 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-hash extension 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 400 graphql_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§

GraphQLChecker
A compiled GraphQL check for one route, built from the route’s RouteGraphql config block. Cheap to construct (clones the config values); held by the request path for the duration of one check.
GraphqlBody
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_bytes and the reconstructed body is forwarded to the rest of the request path.

Enums§

GraphQLBodyError
Error returned when the body cannot be read for the GraphQL check.
GraphQLCheckResult
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).