GraphQL's Six Default Attack Surfaces
A security team disables GraphQL introspection in production and considers the schema reconnaissance surface closed. Two hours later a researcher submits a bounty report with the complete type map, reconstructed field-by-field using error messages and __typename, without any __schema access. The surface was never closed. It was renamed.
GraphQL is not REST with different syntax. Six default behaviors transform any schema into a self-describing attack surface with built-in amplification. Those six: introspection enabled, field suggestions active, alias batching unrestricted, depth unbounded at parse time, field auth absent, and content type unrestricted. Security teams that migrated from REST without addressing these defaults traded known attack surfaces for unknown ones.
Disabling __schema Closes One Tool and Leaves Three Paths Open
The __schema field returns the complete schema in a single unauthenticated request: every type, field, resolver, and argument. HackerOne report #291531 documents exactly this scenario on a major platform in production, with no credentials required.
Three reconnaissance paths remain open even with __schema disabled. The __typename field is mandatory per the GraphQL spec and cannot be disabled without breaking existing client tooling. The Clairvoyance tool reconstructs the full schema by fuzzing field suggestions in server error messages, without any __schema access. Inserting spaces, newlines, or commas after __schema bypasses regex-based introspection blockers, a technique documented by PortSwigger Web Security Academy.
GET requests and x-www-form-urlencoded POST requests bypass introspection restrictions applied only to application/json POST. GraphQL Voyager visualizes the complete schema graph from any introspection response obtained through these paths. Escape.tech scanned 160 public GraphQL APIs in 2024 and found 69% susceptible to DoS via unrestricted resource consumption, reflecting configuration defaults, not attacker sophistication.
Alias Batching: One HTTP Request, 1,000 Resolver Executions
Two batching mechanisms operate in GraphQL, and the distinction determines which defense works. Array batching sends multiple queries as a JSON array in one request and can be blocked by disabling the batch endpoint. Alias batching is structurally different: a single GraphQL document can contain 1,000 distinctly named mutations. That document arrives as one HTTP request from one IP address.
Per-IP rate limits count HTTP requests. One request with 1,000 aliases counts as 1 against the limit and triggers 1,000 resolver executions on the server. HackerOne report #2166697 quantifies the impact: 6,400 reports created in approximately 100 HTTP requests in 40 seconds, reported against HackerOne's own platform. HackerOne #481518 (Shopify) demonstrates the same pattern by abusing query cost buckets to bypass per-operation rate limits.
Field-level rate limiting, not HTTP request-level rate limiting, is the control that stops this vector. The OWASP GraphQL Cheat Sheet recommends limiting batchable operations and preventing batching for sensitive objects. Without this control, any login, account creation, or token validation endpoint is exposed to brute force from a single IP address.
Recursive Fragments Attack the Query Planner, Not the Executor
This is the distinction that separates a real analysis from generic "limit your query depth" advice. Deeply nested queries and recursive named fragments bypass per-operation timeouts because the DoS happens during the planning phase, before any resolver execution begins. Timeout defenses fail because the planner exhausts resources before the timeout fires.
CVE-2023-28867 affects graphql-java before version 20.1 (CVSS 7.5) and documents stack consumption via crafted queries, fixed in versions 20.1, 19.4, 18.4, and 17.5. CVE-2025-32032 affects Apollo Router before version 1.61.2 (CVSS 7.5) and exposes thread pool exhaustion during query planning via nested named fragments. The Apollo Router planner has no configurable timeout: a handful of malformed queries renders the router inoperable. CVE-2023-26144 affects graphql-js before version 16.8.1 (CVSS 5.3) via the OverlappingFieldsCanBeMergedRule in the reference JavaScript implementation.
Fragment cycles, where A includes B and B includes A, exploit O(n²) or exponential complexity in schema validation. Only depth limits applied at parse time, before planning begins, reliably stop this vector.
Object-Level Auth Without Field-Level Auth Creates GraphQL-Specific BOPLA
Resolvers that enforce object-level authorization but omit field-level checks create a BOPLA surface unique to GraphQL. An attacker authorized to read a User type can request administrative fields never exposed in the application UI.
OWASP API Security 2023 API3 (Broken Object Property Level Authorization) treats each GraphQL field resolution as an independent authorization decision. HackerOne #2207248 (Shopify) documents an IDOR via BillingDocumentDownload and BillDetails queries, rewarded with a $5,000 bounty. The Best Bug award at HackerOne Ambassador World Cup 2023 went to a GraphQL authentication bypass found at AS Watson via the same field-without-authorization pattern. The underlying pattern is consistent: the object resolver checks resource ownership; the field resolver checks nothing.
When introspection is active, discovery cost drops to zero: the schema exposes administrative or deprecated fields absent from any application screen. Mitigation requires per-field authorization directives, using @auth in Apollo, graphql-shield as middleware, or custom resolver guards.
GraphQL over GET Is CSRF and Persisted Query Endpoints Widen the Injection Surface
GraphQL APIs accepting queries via GET or non-JSON content types are CSRF-vulnerable without a token. An HTML form on an external domain triggers authenticated mutations via GET with the query payload encoded in the URL, without any custom headers required.
Apollo Server 2/3 had CSRF via multipart/form-data upload (GHSA-2p3c-p3qw-69r4). Apollo Server 4 enables csrfPrevention: true by default; earlier versions require explicit configuration. CVE-2024-23841 affects @apollo/experimental-nextjs-app-support before version 0.7.0 (CVSS 8.2) and demonstrates XSS via JSON injection in SSR from a GraphQL response. CVE-2025-32380 exposes validation DoS in Apollo Router via malformed persisted query payloads.
Persisted query endpoints that trust client-supplied SHA-256 hashes without server-side validation let attackers register arbitrary queries through a channel considered trusted. Defense requires three simultaneous controls: POST with application/json only, server-side Content-Type validation, and server-registered persisted queries that reject unknown client-supplied hashes.
Per-Implementation Hardening: None of the Critical Controls Are On by Default
Every major GraphQL implementation ships the tools to close these surfaces. None enables the critical controls by default, making security configuration an explicit developer decision.
In Apollo Server, introspection is auto-disabled at NODE_ENV=production since version 3. Depth limiting requires the external graphql-depth-limit package. csrfPrevention defaults to true only in v4. In graphql-java, MaxQueryDepthInstrumentation and MaxQueryComplexityInstrumentation are in the core library: recommended maximum depth of 7, maximum complexity of 100. In Hasura, HASURA_GRAPHQL_ENABLE_ALLOWLIST=true enforces a pre-approved operation list; per-role introspection is configurable via Security > Schema Introspection in the console. In gqlparser (Go/gqlgen), CVE-2023-49559 was patched in 2.5.13 with a 15,000-token limit, with permanent token-limit configuration available in 2.5.15.
The MAGO team tool (mago.team) detects GraphQL endpoints, maps the full schema, and automatically tests alias batching for rate limit bypass. No credentials required. Escape.tech (2024) found that 80% of discovered issues were preventable with access controls, input validation, and rate limiting. The controls exist across all implementations. The defaults omit them across all implementations.
A GraphQL security audit is not a REST audit with different syntax. Hardening a GraphQL endpoint requires six independent checks, each at a different layer of the stack:
- Introspection disabled in production
- Field suggestions suppressed
- Alias batching rate-limited at field level
- Query depth bounded at parse time
- Per-field resolver authorization
- Content type restricted to POST with
application/json
Fixing one without the others produces compliance theater, not security.
Top comments (0)