A penetration tester sends a POST with the SHA-256 hash of a deeply-nested query to an endpoint where introspection is blocked. The server replies with PERSISTED_QUERY_NOT_FOUND. She resends with the hash and the full query. The server executes, caches it. The allowlist never fired.
Teams disable GraphQL introspection believing it prevents schema discovery. The APQ registration endpoint accepts arbitrary query strings at runtime. It creates a parallel channel that exposes schema structure, bypasses allowlists, and skips complexity re-validation on subsequent lookups. Apollo's own documentation warns: APQ and safelisting are mutually exclusive.
APQ Is a Performance Tool With No Security Contract
The APQ registration protocol accepts any query string on the first submission and caches it by SHA-256. This design decision is incompatible with access control because the registration step has no authorization gate in default configurations.
The protocol works in 2 requests: the client sends the hash and the server replies with PERSISTED_QUERY_NOT_FOUND. The client resends the hash plus the full query string; the server executes and caches it. Apollo's documentation is explicit: "APQ does not provide any security features; the benefit of using them is to reduce network overhead." The APQ cache is built from client-submitted queries, not from a pre-approved registry.
To enable safelisting, Apollo GraphOS documentation is direct: you must disable APQs. The 2 features are mutually exclusive by design.
Three Schema Channels Survive After Introspection Is Disabled
Disabling introspection blocks one specific query type (__schema, __type) while leaving 3 independent schema-disclosure paths open: APQ registration responses, field suggestions, and alternative schema endpoints.
CVE-2026-35413 (Directus) documents this directly. GRAPHQL_INTROSPECTION=false blocked __schema but not the endpoint /graphql/system?query=server_specs_graphql, which returned the full SDL including collection names, field types, and relationships without authentication. The fix in Directus 11.16.1 aligned SDL endpoint access control with the introspection setting.
The field suggestion attack requires neither APQ nor introspection. GraphQL returns messages like {"errors":[{"message":"Did you mean 'searchUsers'?"}]} for misspelled fields, revealing valid names. The Clairvoyance tool automates this fuzzing to recover full schemas from any reachable endpoint.
Registering Arbitrary Queries Writes Into the Cache the Allowlist Was Meant to Protect
When a server runs APQ alongside a query allowlist, the registration endpoint gives attackers a direct write path into the query cache. The allowlist controls which hashes execute, but the attacker controls what those hashes point to.
Apollo's 2-phase registration accepts the raw query string during phase 2, before any allowlist check determines whether the hash is permitted. The client generates the hash, so the attacker fully controls the namespace mapping hash to query. Apollo GraphOS documentation confirms that APQs let clients register arbitrary operations at runtime while safelisting restricts operations to those explicitly registered. Enabling both simultaneously undermines the safelist.
The attack runs in 4 steps. Compute the SHA-256 of a malicious query locally. Send only the hash and receive PERSISTED_QUERY_NOT_FOUND, then resend the hash plus the full query. The server executes and caches it; subsequent requests use only the hash. The cache becomes an attacker-controlled allowlist.
Complexity Analysis Runs Once at Registration, Not on Every Lookup
Query depth validation runs at registration. Servers that skip re-validation on APQ hash lookups issue a one-time pass that attackers replay without cost enforcement. A marginally-complex query becomes a sustained load vector.
CVE-2025-32034 (Apollo Router, CVSS 7.5) documents the mechanism. Named fragments with deep nesting bypassed the query planner's internal optimization, exhausting the thread pool with a small number of crafted requests. Patched in Apollo Router 1.61.2 and 2.1.1.
CVE-2026-32594 (Parse Server, CVSS 6.9) exposes the same pattern over WebSocket. The subscriptions endpoint did not pass through the Express middleware that enforces complexity limits. APQ hash-only requests carry no query body for the parser to analyze. If the server skips the complexity check on cache hits, registered queries execute without cost enforcement.
Subscription Endpoints Multiply the Same Gap Over WebSocket
GraphQL subscriptions delivered over WebSocket frequently bypass the HTTP middleware layer that enforces authentication, introspection controls, and complexity limits. APQ-adjacent vulnerabilities apply to a surface many teams do not test.
CVE-2026-32594 (Parse Server) demonstrates the pattern. The WebSocket subscriptions endpoint did not route through Express middleware. Unauthenticated clients could enable introspection, bypass complexity limits, and execute arbitrary operations.
CVE-2026-5423 (@neo4j/graphql, CVSS 8.2) exposes the authentication surface. Subscriptions accepted connectionParams.jwt without signature verification, allowing clients to forge arbitrary role claims and bypass the @authentication and @subscriptionsAuthorization directives. Affected versions: v5.0.0 through 5.12.13 and v7.0.0 through 7.5.5. All 4 CVEs share the same root: security controls applied at the HTTP layer do not reach alternate protocols.
Detection: Standard Scanners Miss the Registration Endpoint
graphql-cop and similar tools test introspection status and field suggestions, but do not by default probe whether APQ registration accepts arbitrary queries. This gap routinely survives automated assessments.
graphql-cop's checks cover enabled introspection, alias overload, batching, CSRF, and field suggestions. There is no APQ registration test in the default configuration. Manual detection: compute the SHA-256 of a complex nested query locally. Send the POST with only the hash and observe PERSISTED_QUERY_NOT_FOUND versus rejection. Then send the hash plus the full query and verify whether the server executes or rejects it.
A valid data response on the second request is the positive signal. APQ registration is open and the allowlist is not enforced at registration time. The MAGO Intel tool (intel.mago.team) probes GraphQL endpoints for APQ support and open registration. It identifies servers where schema discovery is possible even when introspection is disabled.
Fix: Gate Registration, Not Just Execution
The correct defense is layered. Disable APQ when operating a safelist. Require the same authentication on the registration request as on mutations, and run complexity analysis on every APQ lookup, not only at initial registration.
Apollo's official guidance is explicit: set apq: { enabled: false } in the router config when using Persisted Query Lists. The authentication gate on registration is critical. APQ phase 2 (hash plus query string) must require the same session or token as a mutation. Unauthenticated APQ registration is the root cause of arbitrary-query injection.
Complexity re-validation on every lookup: middleware should run the cost analyzer against the cached query string on every request. The CVE-2026-35413 fix aligned SDL endpoint access control with the GRAPHQL_INTROSPECTION setting. Apply the same pattern to any non-standard schema exposure path. For WebSocket, ensure the upgrade handler applies the same middleware chain as the HTTP handler, the root cause of CVE-2026-32594.
Disabling introspection is a checkbox on a security questionnaire. APQ registration is an open query interface. The choice is not a toggle: run APQ for performance or run a safelist for security, but not both.
Top comments (0)