GraphQL persisted-query bypass
TL;DR: Persisted queries (Apollo APQ, Relay PQ, GraphQL HTTP spec PQ) are a performance / security feature: clients send a hash; the server looks up the corresponding query. Servers configured to only accept persisted queries (allowlist mode) effectively block introspection, ad-hoc queries, and many bug-bounty test vectors. Common bypasses: extension parameter manipulation, query-string vs body mismatch, double-encoding, and Apollo automatic-persisted-queries (APQ) registration race. Companion to graphql-attacks and graphql-source-review.
Why this matters
- Persisted queries are increasingly the default for modern GraphQL deployments.
- Allowlist-only mode is presented as a security control.
- Pentesters and hunters need bypass paths to attack PQ-only endpoints.
- Bug-bounty programs flag PQ-only as in-scope but harder to fuzz.
Persisted queries recap
Three flavours:
Apollo APQ (Automatic Persisted Queries)
Client first time: send query body with extensions.persistedQuery.sha256Hash. Server stores hash → query. Client next time: send only hash.
In automatic mode, anyone can register a new query. In allowlist mode, the server only accepts hashes already in a server-side list (uploaded at deploy time).
Relay persisted queries
Compile-time generation of query IDs; client uses ID; server has a static map.
GraphQL-over-HTTP spec persisted operations
Standardised version. Same idea: hash or ID lookup.
Class 1 — APQ registration when expected to be allowlist
Some “allowlist” Apollo configurations still accept new query registration if a specific flag isn’t set. If the server allows registration:
- Send your query body with the SHA-256 hash; server registers.
- Now you can send arbitrary queries.
Audit: test by sending a brand-new query with hash; see if it executes.
Class 2 — Query in extensions parameter
Some servers accept the query in both query body field and extensions.persistedQuery field. If the parser preference order is wrong:
- Send hash for persisted query A.
- Send body
queryfor a different query B. - Server may execute B while looking up A.
Test with conflicting body + extensions.
Class 3 — Query-string vs body mismatch
GET requests can pass the query in URL. Different parsing of GET vs POST in some servers:
- Hash in URL; body in POST has the actual query.
- Server reads URL for hash, ignores body — or vice versa, depending on implementation.
Test mixed transport.
Class 4 — Variables-as-query
Some implementations stringify variables and substitute into the query template. If you can pass query-like text in a variable:
- Inject GraphQL syntax that becomes part of the resolved query.
- Effectively unauthorised operation.
Rare but documented in specific implementations.
Class 5 — Hash collision / wildcard
For SHA-256 hashes, collisions are infeasible. But:
- Some implementations use shorter hashes (MD5, custom truncation).
- Some implementations use
**style “wildcards” or normalize queries before hashing.
If normalisation strips comments / whitespace, you can inject content that’s part of the query but stripped before hash computation:
query { x } # actually query { y }— hashing strips comment; server storesquery { x }but executes the comment too in some flawed parsers.
Mostly historical; modern implementations harden against.
Class 6 — Operation name swap
If a persisted document contains multiple operations (query A, query B, mutation C), the request specifies operationName. Bypass:
- Allowlist a benign query.
- Request the same allowlisted document but with
operationName: mutation_C— if document contains mutation, it executes.
Audit document allowlist for unintended operations.
Class 7 — Custom directives / fragments
Some servers allow fragments to be defined in the persisted doc but spread into the executed query at runtime by client. Crafted spreads can expand to unintended queries.
Class 8 — Apollo Federation gateway bypass
In a federated GraphQL setup, the gateway enforces allowlist; subgraphs may not. If you can reach a subgraph directly (often on internal port):
- Subgraph executes without allowlist.
Test for unauthenticated subgraph endpoints.
Class 9 — Introspection via “magic” queries
Introspection is usually blocked in allowlist mode. But:
- Error messages may leak schema info (“field X not found on type Y”).
- Fuzz field names against error responses.
- 4xx error message bodies sometimes contain partial schema.
Class 10 — Cache-key confusion
PQ deployments often add a CDN cache in front. Cache-key confusion (cache-poisoning-modern-chains):
- Hash in URL; CDN caches by URL.
- Body in POST is different per request, but cache hits return prior cached response.
- Privacy: response intended for one user served to another.
Or the reverse for cache poisoning of authenticated responses.
Recon approach
Quick checks against a suspected PQ-allowlist endpoint:
- Send a brand-new query with
extensions.persistedQuery.sha256Hash. See if accepted. - Send hash for a known operation with a different body query. See preference.
- Send GET with hash in URL and POST body conflicting. See preference.
- Send a persisted operation name with
operationNamefor a different operation in the document. - Probe internal / subgraph endpoints.
- Probe introspection paths.
Tools: Burp + GraphQL extensions (InQL, GraphQL Voyager), graphw00f, custom scripts.
Defensive baseline
- Strict allowlist mode — disable automatic registration.
- Single operation per persisted document — no multi-operation documents.
- Server-side enforcement at every subgraph, not just gateway.
- Same parsing precedence for body, extensions, URL.
- Block introspection in production.
- No GET requests for queries with side effects.
- Cache key includes body hash or operation name + variable hash.
- Subgraph network isolation — only reachable from gateway.
Workflow to study
- Deploy an Apollo GraphQL server with APQ in allowlist mode.
- Implement a small client.
- Test each bypass class above against the deployment.
- Tighten configuration to defend against each.
- Repeat against a federated setup.
Related
- graphql-attacks — general class.
- graphql-source-review — source audit.
- graphql-batching-aliasing-abuse — adjacent.
- graphql-field-permissions-introspection — adjacent.
- cache-poisoning-modern-chains — adjacent.
- api-fuzzing-wide-vs-deep — methodology.