GraphQL API Exploitation: A Practical Hunting Methodology

A 200 OK response means absolutely nothing anymore. If you fire up Burp Suite against a modern application relying heavily on GraphQL, your proxy history will just be an endless sea of identical POST /graphql requests, all happily returning 200 OKs regardless of whether your query succeeded, failed, or threw a massive syntax error. Traditional Dynamic Application Security Testing (DAST) scanners absolutely hate this pattern. They rely on 401s, 403s, and 500s to infer state. When a scanner hits a GraphQL endpoint, it effectively goes blind.
GraphQL was designed by Facebook to solve the over-fetching problem of REST APIs, allowing clients to request exactly what they need in a single query. But this architectural shift fundamentally changes how we hunt for bugs. The attack surface collapses from hundreds of distinct REST endpoints into a single monolithic endpoint powered by a complex, nested graph of "resolvers."
This methodology covers how to systematically test authorized GraphQL endpoints, from mapping the schema to exploiting query batching and hunting for logic flaws.
The Scope Rules & The Goal
Before touching anything, remember: GraphQL endpoints are notoriously fragile. Complex nested queries can trigger resource exhaustion (Denial of Service) very quickly. When executing authorized bug bounty or pentest workflows, your goal is to identify unauthorized data access, Broken Object Level Authorization (BOLA), and rate-limit bypasses—not to crash the production database by nesting queries 100 levels deep.
Stage 1: Mapping the Attack Surface (Introspection)
The biggest gift a developer can give you is leaving GraphQL Introspection enabled in production. Introspection is a built-in feature that allows you to query the GraphQL server for its entire schema—every object, query, mutation, and argument it supports.
To test for introspection, send a POST request with the __schema meta-field:
// POST /graphql { "query": "\n query IntrospectionQuery {\n __schema {\n queryType { name }\n mutationType { name }\n types {\n ...FullType\n }\n }\n }\n \n fragment FullType on __Type {\n kind\n name\n description\n fields(includeDeprecated: true) {\n name\n args {\n ...InputValue\n }\n }\n }\n \n fragment InputValue on __InputValue {\n name\n type { ...TypeRef }\n }\n \n fragment TypeRef on __Type {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n }\n }\n }\n " }
Note: This is an abbreviated version of the standard introspection query. Most tools send a much larger payload.
If it returns the full schema, you just saved hours of work.
An attacker leveraging an exposed introspection query to map the entire GraphQL schema and identify hidden mutations.
If introspection is disabled, you have to work harder. You will need to rely on error message verbosity (which GraphQL is infamous for). If you query a field that doesn't exist, the server often responds with "Did you mean X?". Tools like Clairvoyance automate this by fuzzing the endpoint and using these verbose errors to reconstruct the schema word-by-word. This technique is similar to how we use ffuf for web fuzzing, but specialized for the GraphQL dictionary.
Once you have the schema, don't read raw JSON. Load it into a tool like InQL (a Burp Suite extension) or GraphQL Voyager. These tools visualize the nodes and edges, instantly highlighting isolated administrative mutations or deprecated queries that developers forgot to remove.
Stage 2: Bypassing Rate Limits via Query Batching
GraphQL supports a feature called query batching, which allows a client to send an array of queries in a single HTTP request. The server executes them and returns an array of responses.
Imagine you're the on-call engineer writing a rate-limiting rule on your Web Application Firewall (WAF) or API gateway. You tell it: "Block any IP that makes more than 5 POST requests to /graphql per minute."
The attacker sends exactly one HTTP POST request, but the JSON body contains an array of 500 login attempts.
Visualizing how an array of batched queries easily slips past an HTTP-level rate limiter.
Here is a benign example of how to format a batch request. Instead of a single JSON object, you pass a JSON array:
// POST /graphql // A batched request attempting to brute-force a 2FA OTP code [ { "query": "mutation { verifyOtp(code: \"1000\") { success } }" }, { "query": "mutation { verifyOtp(code: \"1001\") { success } }" }, { "query": "mutation { verifyOtp(code: \"1002\") { success } }" } ]
If the server supports batching, it will process all three mutations and return an array of three JSON responses. Because only a single HTTP request was made, the IP-based WAF rate limiter is completely bypassed.
Alias Batching: If the server rejects JSON arrays, you can use GraphQL aliases to batch multiple queries inside a single JSON object.
// POST /graphql { "query": "mutation {\n attempt1: verifyOtp(code: \"1000\") { success }\n attempt2: verifyOtp(code: \"1001\") { success }\n attempt3: verifyOtp(code: \"1002\") { success }\n}" }
Both techniques are devastating for endpoints handling login, OTP verification, or coupon code redemption.
Stage 3: Hunting for BOLA in Resolvers
Broken Object Level Authorization (BOLA), often referred to as IDOR (Insecure Direct Object Reference), is rampant in GraphQL. You can read more about the core concepts of this in our API Hunting Methodology.
In REST, a developer writes an authorization check on GET /api/users/123. In GraphQL, data is fetched via individual "resolvers" that attach to fields. A developer might properly secure the top-level user(id: 123) query to ensure you can only fetch your own user ID.
But what about the nested connections?
query { project(id: 456) { name owner { email passwordHash privateMessages { content } } } }
If you have permission to view project(id: 456), the top-level resolver passes. But when the GraphQL execution engine traverses down to owner, and then to privateMessages, does the privateMessages resolver explicitly check if the current user is the owner? Often, developers assume that if the top-level query was authorized, the deeply nested queries are safe too. This assumption causes massive data leaks.
To hunt for these, take every node in the graph and try to access it via different root queries. If you can't query user(id: 999) directly, see if you can reach user 999 by querying a public post they authored and traversing backward through the graph (post(id: 1) -> author -> email).
How Defenders Catch This
Defending GraphQL is significantly harder than defending REST. Standard WAF rules based on URLs and HTTP verbs do not work.
A secure implementation must apply authorization checks at the data-access layer (the models or ORM), not inside the GraphQL resolvers. If the data-access layer knows the current user's context, it won't matter how the attacker traverses the graph; the database query will simply refuse to return rows belonging to someone else.
To catch introspection and batching abuse, defenders need to parse the GraphQL payload before executing it. Many modern gateways provide specific configuration blocks for GraphQL. For example, disabling introspection entirely in production and setting a max_query_depth and max_batch_size.
Here is a generic Suricata signature to detect standard introspection attempts (useful for SOC analysts monitoring lateral movement or internal testing):
# Detect standard __schema introspection queries in HTTP POST bodies alert http $EXTERNAL_NET any -> $HTTP_SERVERS $HTTP_PORTS (msg:"ET SCAN GraphQL Introspection Query Detected"; flow:established,to_server; content:"POST"; http_method; content:"/graphql"; http_uri; content:"__schema"; http_client_body; classtype:web-application-attack; sid:1000001; rev:1;)
The Verdict
GraphQL is an incredibly powerful query language, but it places a massive burden on the developer to get authorization right at every single node in the graph.
Turn off introspection in production, enforce strict query depth limits, and never trust that your WAF's rate-limiting rules will hold up against a batched query array. If you are authorized to test APIs, put down the automated scanners and start manually reading the schema. The best bugs in GraphQL are pure logic flaws.
Related Blogs
- API Hacking Methodology: Hunting for BOLA
- Assetnote Kiterunner: API Security Discovery
- FFUF Web Fuzzing Masterclass
References / Further Reading
- GraphQL Official Documentation on Introspection. GraphQL Foundation. https://graphql.org/learn/introspection/
- PortSwigger Web Security Academy: GraphQL API Vulnerabilities. PortSwigger. https://portswigger.net/web-security/graphql
- InQL - A Burp Extension for GraphQL Security Testing. Doyensec. https://github.com/doyensec/inql
- Clairvoyance - GraphQL Schema Guessing Tool. Nikita Stupin. https://github.com/nikitastupin/clairvoyance


