GraphQL ships two attack surfaces that REST APIs do not have: introspection and query batching. Disclosed CVEs and bug bounty reports show these surfaces chain into rate limit bypass, account takeover, and RCE in production. They are not just informational findings.
A single HTTP request carrying 1,000 login attempts reaches the GraphQL API. The rate limiter counts one request and lets it through. The credential store processes 1,000 attempts and one of them matches. This is not a zero-day. It is the GraphQL specification working exactly as designed.
GraphQL Ships Attack Surface by Specification, Not by Misconfiguration
Introspection and batching are not implementation bugs. They are mandatory features of the GraphQL specification. Any server that correctly implements the spec also implements the attack surfaces. Disabling either requires actively deviating from framework defaults.
The GraphQL spec requires the __schema endpoint for schema discovery. Array batching and alias batching are spec-compliant patterns. REST APIs have no equivalent built-in schema exposure mechanism. They also have no operation multiplication in a single HTTP request. Each REST endpoint exposes one operation per request, without exception.
In March 2025, CVE-2025-27407 (CVSS 9.0) demonstrated that introspection goes beyond information disclosure. GraphQL::Schema.from_introspection() in graphql-ruby executes arbitrary Ruby from a malicious schema JSON (CWE-94). An attacker who controls which schema a client fetches reaches remote code execution. The patch required multiple backport versions: 2.4.13, 2.3.21, 2.2.17, 2.1.15, 2.0.32, and 1.x branches.
The OWASP GraphQL Cheat Sheet recommends disabling introspection and GraphiQL in production, confirming that the default is enabled. The recommendation exists because removing something active requires explicit action. The developer who disables introspection and considers the problem solved is addressing the wrong question.
Introspection in Production Maps Every Type, Mutation, and Relationship to the Attacker
Introspection enabled in production gives the attacker a complete API blueprint. Internal types, undocumented mutations, relationship graphs, and field types become accessible in a single unauthenticated query. That knowledge is the prerequisite for every targeted attack that follows.
H1 #291531 showed a production endpoint returning the full schema via __schema, including internal admin types, with no authentication. H1 #1132803 confirmed the same pattern in another program: introspection accessible in production, internal query structure exposed. CVE-2024-50312 (CVSS 5.3) affected the OpenShift console, where unauthenticated users retrieved the complete list of available queries and mutations.
The introspection query is one line:
{__schema{types{name,fields{name,type{name,ofType{name}}}}}}
The result maps the entire attack surface in a single unauthenticated response. This is not partial exposure. It is the equivalent of internal API documentation delivered to the attacker without an authentication requirement.
CVE-2025-27407 elevates the severity from informational to critical. In deployments where Ruby clients fetch remote schemas via GraphQL::Schema.from_introspection(), an attacker who controls the schema endpoint reaches RCE. The vector is network, no privileges required. Full schema plus remote code execution form a chain that starts with a __schema query.
Two professional tools convert schema into attack surface systematically. graphql-cop (github.com/dolevf/graphql-cop) tests introspection, field suggestions, alias overloading, and batch queries. It generates cURL reproduction commands per finding, suitable for CI/CD integration. The InQL extension for Burp Suite analyzes the introspection JSON and organizes queries, mutations, and subscriptions into request templates ready for targeted testing.
Alias Batching: 1 HTTP Request, 6,400 Authentication Attempts
Alias batching allows N authentication operations in a single HTTP request from a single IP. IP-based rate limiting fails because the request is one. Per-request rate limiting fails for the same reason. The rate limiter counts one request while the authentication endpoint processes N credential pairs.
H1 #481518 confirmed this in production at Shopify. Batching bypassed cost-based rate limiting per application bucket. Shopify triaged and fixed it, confirming exploitability in production and not just in a lab.
H1 #2166697 quantified the amplification on the HackerOne platform itself. Alias batching produced approximately 75 report creations per HTTP request. With Turbo Intruder, the researcher reached over 6,400 operations per burst while the HTTP rate limiter recorded one request. The ratio of 6,400 operations to 1 HTTP request is the metric that summarizes the problem. HTTP-count-based defenses are useless in this model.
The payload structure:
mutation {
a: login(input: {email: "[email protected]", password: "aaa"}) { token }
b: login(input: {email: "[email protected]", password: "aab"}) { token }
c: login(input: {email: "[email protected]", password: "aac"}) { token }
}
Each alias executes independently. The server returns all results in the same JSON response. There is no limit on the number of aliases a client can include by default.
The PortSwigger WebSecAcademy lab documents the pattern with a functional payload: bruteforce0:login(...), bruteforce1:login(...), in a mutation block. Wallarm documented the same vector for OTP bypass: all 6-digit variants of a TOTP sent as aliases. The per-request throttle records one attempt. The OTP system processes every possible combination.
The defenses that fail are IP rate limits, per-request CAPTCHA, and WAF rules based on HTTP request rate. All of them count HTTP transactions, not GraphQL operations. Counting at the wrong layer guarantees the bypass.
Recursive Types Allow Exponential Resolver Cost from Linear Input
Recursive types in GraphQL allow O(n^k) resolver invocations from O(n) bytes of input. Two CVEs in 2025 confirm this is exploitable in production libraries without custom tooling.
CVE-2025-31496 (Apollo Compiler, CVSS 7.5) affected named fragment processing. Deeply nested and reused fragments are processed exponentially per spread. The patch landed in apollo-compiler v1.27.0 in April 2025. The vector is network, no privileges required.
CVE-2023-26144 (graphql npm, CVSS 5.3) affected the OverlappingFieldsCanBeMergedRule validation in versions 16.3.0 to 16.8.0. The rule processed queries with extensive overlapping fields without resource limit guards. H1 #3287208 confirmed DoS via mutation aliases on the HackerOne platform itself in 2025, disclosed under the program's updated DoS policy.
The attack pattern follows progressive nesting:
query {
user {
friends {
friends {
friends {
posts {
comments {
author { friends { ... } }
}
}
}
}
}
}
}
Resolver fan-out multiplies at each nesting level. Depth of 7 or more makes processing prohibitive for the server. A 500-byte query can generate hundreds of database calls.
Depth limits, such as graphql-depth-limit in JavaScript and MaxQueryDepthInstrumentation in Java, fix the maximum depth between 7 and 10. Complexity analysis assigns cost budgets per field and rejects expensive queries before execution. Depth alone is not sufficient: wide queries with few levels but many fields per level also exhaust resources.
Disabling Introspection Is the Wrong Mental Model
The most common mitigation, disabling introspection, removes schema disclosure but leaves alias batching intact. An attacker with a mutation name from public documentation executes 1,000 login attempts. Introspection status does not change that number.
graphql-cop tests 4 distinct surfaces: introspection, field suggestions, alias overloading, and batch queries. Disabling introspection removes only 1 of them. The other 3 remain active and exploitable without schema discovery.
Clairvoyance reconstructs the schema from field suggestions even when __schema returns 403. The server still suggests valid field names in error messages. Clairvoyance uses these responses to reconstruct the schema through systematic enumeration. Disabled introspection does not mean hidden schema.
CVE-2025-27407 (CVSS 9.0) adds a vector that persists after disabling introspection on the server. The RCE affects Ruby clients that fetch remote schemas via GraphQL::Schema.from_introspection(). Internal servers consuming third-party schemas remain exposed to CWE-94. The patch covers graphql-ruby 2.4.13 and above.
Persisted queries, as an allowlist of approved operation hashes on the server, block both arbitrary batching and custom introspection. They are the only mitigation that addresses both surfaces simultaneously. Only pre-approved operations are accepted. Any query outside the allowlist returns an error before reaching the resolvers. The implementation is compatible with Apollo Server, GraphQL Yoga, and graphql-ruby.
The MAGO team tool detects GraphQL endpoints via Content-Type, tests introspection, and runs alias batching probes against authentication endpoints.
The patch for introspection is one line of configuration. The patch for alias batching requires an architectural change: persisted queries, operation allowlists, or per-alias rate limit counting at the resolver level. Most teams ship the one-line fix and consider GraphQL protected. The CVEs and H1 reports say otherwise.
Top comments (0)