GraphQL has changed how a lot of modern applications structure their APIs, and with that shift has come a set of security testing challenges that don’t map cleanly onto traditional REST methodology. I’ve tested a good number of GraphQL APIs over the past few years, and the pattern I keep seeing is teams applying REST-style security thinking to an API paradigm that behaves very differently — with consequences ranging from information disclosure to full denial of service. In this guide, I’ll walk through GraphQL penetration testing from the ground up: what makes it different, how to enumerate the schema, and the specific vulnerability classes worth testing for.
What GraphQL Is and Why Its Security Model Differs
GraphQL is a query language and runtime for APIs that lets clients request exactly the data they need through a single endpoint, rather than hitting many different REST routes. Instead of GET /users/1042, GET /users/1042/orders, and GET /users/1042/profile as three separate REST calls, a GraphQL client sends one query specifying exactly which fields and nested relationships it wants, and the server resolves them all in a single response.
This flexibility is exactly what makes GraphQL security testing different:
- A single endpoint, typically
/graphql, replaces dozens of REST routes, which changes how you approach attack surface mapping. - Clients define query shape, which introduces resource exhaustion risks that don’t really exist the same way in REST — a single malicious query can request deeply nested or repeated data that overwhelms the server.
- Introspection can expose the entire schema, including fields and types never intended for public documentation, effectively handing an attacker a complete API map.
- Authorization is field-level, not endpoint-level, meaning access control has to be enforced per-field/per-resolver rather than per-route, which is easy to get wrong.
Lab Setup for Legal Practice
I practice GraphQL testing exclusively against deliberately vulnerable or self-hosted targets:
- DVGA (Damn Vulnerable GraphQL Application) — purpose-built with a wide range of GraphQL-specific vulnerabilities.
- A self-hosted Apollo Server or GraphQL Yoga instance that I configure with intentionally weak authorization logic for practice.
- Burp Suite with the InQL extension, which is specifically designed for GraphQL schema exploration and query generation.
- GraphQL Voyager, for visualizing schema relationships once introspection data is obtained.
docker run -d -p 5013:5013 -p 5000:5000 dolevf/dvga
What this does: launches DVGA locally with its web interface and GraphQL endpoint exposed, giving you a legal, richly vulnerable target covering most GraphQL attack classes.
Methodology: Step-by-Step GraphQL Testing
Step 1: Discover the GraphQL Endpoint
GraphQL endpoints commonly live at predictable paths. I check these during reconnaissance:
/graphql
/graphql/console
/api/graphql
/graphiql
/v1/graphql
Purpose: confirming which path the API uses is the first step, since most subsequent testing (introspection, query crafting) depends on knowing the exact endpoint.
Step 2: Test for Introspection
Introspection is a built-in GraphQL feature that lets clients query the schema itself — a huge convenience for development, and a serious information disclosure risk if left enabled in production.
curl -X POST https://lab.local/graphql \
-H "Content-Type: application/json" \
-d '{"query":"{__schema{types{name,fields{name}}}}"}'
Purpose: this sends a standard introspection query requesting every type and field defined in the schema. If introspection is enabled, the response reveals the entire API surface — including internal-only mutations, admin fields, and data relationships that were never meant to be publicly documented.
For a fuller, more readable schema dump, I use InQL directly within Burp, which sends the complete introspection query and generates a query template for every discovered operation automatically.
Step 3: Enumerate Queries and Mutations
Once I have the schema (via introspection or, if it’s disabled, via other means — see Step 4), I catalog every available query (read operation) and mutation (write operation), paying particular attention to anything suggesting administrative or sensitive functionality:
mutation {
updateUserRole(userId: "1042", role: "admin") {
id
role
}
}
Purpose: identifying mutations like this early tells you exactly where to focus authorization testing — a role-modification mutation is a prime candidate for privilege escalation testing.
Step 4: Test Schema Discovery Without Introspection
Well-configured production APIs often disable introspection. When that’s the case, I fall back to alternate discovery techniques:
- Field suggestion abuse: many GraphQL servers return “did you mean” error messages when a field name is slightly wrong, which can be leveraged to brute-force valid field names one character at a time.
- Wordlist-based query brute-forcing using tools like
clairvoyance, which reconstructs a schema by iteratively testing common field and type names against the API even with introspection disabled.
clairvoyance -o schema_output.json https://lab.local/graphql
Purpose: this systematically probes the endpoint with a large wordlist of likely field and argument names, reconstructing an approximate schema even when introspection has been properly disabled — useful for demonstrating that disabling introspection alone isn’t a complete fix.
Step 5: Test for Broken Access Control at the Field Level
This is the GraphQL-specific evolution of a classic REST vulnerability. Because a single query can request multiple nested resources, I test whether authorization is enforced consistently across every resolver, not just the top-level query:
query {
user(id: "1042") {
email
orders {
id
paymentMethod {
cardNumber
}
}
}
}
Purpose: this tests whether nested field resolvers (like paymentMethod.cardNumber) apply the same authorization checks as the top-level user query, or whether once you’re “inside” a permitted query, deeply nested sensitive fields are returned without additional checks — a very common oversight in resolver-based authorization models.
Step 6: Test for IDOR via Object Identifiers
Just like REST, GraphQL queries often accept an ID argument. I test whether changing that ID returns another user’s data despite my own authenticated session:
query {
order(id: "1099") {
id
userId
total
}
}
Purpose: if this returns order 1099 despite belonging to a different user than the one authenticated in the request, it confirms an IDOR vulnerability in the resolver logic — the query executed successfully but ownership was never verified.
Step 7: Test for Denial of Service via Query Complexity
GraphQL’s flexibility becomes a liability if the server doesn’t limit query depth, complexity, or aliasing. I test with deeply nested and batched queries:
query {
user(id: "1") {
friends {
friends {
friends {
friends {
friends {
name
}
}
}
}
}
}
}
Purpose: deeply nested queries like this force the server to resolve exponentially growing amounts of data, which can exhaust CPU and memory resources if the server doesn’t enforce a maximum query depth — a GraphQL-specific denial of service vector with no direct REST equivalent.
I also test batching/aliasing abuse, where a single request contains many aliased copies of the same expensive query:
query {
a: expensiveQuery { id }
b: expensiveQuery { id }
c: expensiveQuery { id }
}
Purpose: aliases let a single HTTP request trigger the same costly resolver dozens or hundreds of times, potentially bypassing simple rate limiting that only counts requests rather than query cost.
Step 8: Test for Injection Through GraphQL Arguments
GraphQL doesn’t inherently prevent injection vulnerabilities in resolvers that pass arguments unsafely into database queries or system calls. I test standard injection payloads through query arguments:
query {
searchUsers(name: "' OR '1'='1") {
id
email
}
}
Purpose: this tests whether the name argument reaches a backend SQL query without parameterization — the injection mechanics are identical to REST-based SQLi, just delivered through a GraphQL argument instead of a URL parameter.
Step 9: Test CSRF Against GraphQL Endpoints
A common misconception is that GraphQL’s typical use of POST with a Content-Type: application/json body inherently prevents CSRF. That’s only true if the server strictly enforces content-type checking. Many GraphQL servers, however, also accept queries via GET requests or POST requests with Content-Type: text/plain (which browsers can send cross-origin without triggering CORS preflight), reopening classic CSRF risk.
<form action="https://lab.local/graphql" method="POST" enctype="text/plain">
<input name='{"query":"mutation{updateEmail(email:\"attacker@evil.lab\"){id}}","dummy":"' value='"}'>
<input type="submit">
</form>
Purpose: this crafts a cross-site form submission using text/plain encoding, which browsers permit without a CORS preflight check, to smuggle a valid-looking GraphQL mutation body. If the server accepts this content type and doesn’t validate an anti-CSRF token or check the Origin/Referer header, an attacker can trick an authenticated victim into unknowingly executing a state-changing mutation.
Step 10: Test Subscription-Based Vulnerabilities
For APIs using GraphQL subscriptions (real-time updates over WebSockets), I test whether the WebSocket connection handshake properly re-validates authentication and authorization, since it’s a separate connection lifecycle from standard HTTP requests and is sometimes overlooked in the authorization design:
subscription {
orderUpdates(userId: "1042") {
id
status
paymentDetails
}
}
Purpose: this tests whether subscribing to another user’s real-time update stream succeeds despite being authenticated as a different user, checking whether authorization logic applied to standard queries and mutations was consistently extended to the subscription resolver layer as well — a frequently overlooked gap given that subscriptions are often implemented later and separately from the rest of the schema.
Common Mistakes and Troubleshooting Tips
- Assuming disabled introspection means the API is secure. It removes an easy discovery path but doesn’t fix underlying authorization or injection flaws, and schema reconstruction tools can often work around it anyway.
- Testing only top-level queries. The most impactful GraphQL access control bugs often live in nested resolvers several levels deep in a query.
- Forgetting mutations exist and testing only reads. Mutations are where the highest-impact vulnerabilities (privilege escalation, data modification) usually live.
- Not testing query complexity/depth limits. Many teams focus entirely on data-level authorization and overlook resource exhaustion risks entirely.
- Overlooking batching-based rate limit bypasses. Standard per-request rate limiting doesn’t account for a single request containing dozens of aliased sub-queries.
- Missing GraphQL-specific tools. Trying to test GraphQL purely with REST-oriented tooling misses schema-aware features that InQL and similar extensions provide.
Security Risks and Defensive Recommendations
For development teams building GraphQL APIs, the recommendations that consistently reduce risk:
- Disable introspection in production as a baseline hardening step, while recognizing it’s not a substitute for proper authorization.
- Enforce authorization at the resolver level, consistently, especially for nested and deeply linked fields, not just top-level query entry points.
- Implement query depth limiting and query cost analysis to prevent resource exhaustion from deeply nested or overly complex queries.
- Rate-limit based on computed query cost, not raw request count, to account for aliasing and batching abuse.
- Validate and sanitize all arguments passed into resolvers exactly as you would for any REST parameter, since injection risk doesn’t disappear just because the transport layer changed.
- Use persisted queries in production where feasible, restricting clients to a pre-approved allowlist of queries rather than accepting arbitrary ad-hoc queries.
Throughout this process, I keep detailed notes on the exact query shape used for each finding, since GraphQL queries can get complex quickly and a finding that isn’t precisely reproducible loses most of its value to a development team trying to fix it. I typically include the full raw query and variables alongside each finding in my report, formatted for easy copy-paste back into GraphiQL or Postman for the developer’s own verification.
Testing Federated GraphQL Architectures
Larger organizations increasingly split GraphQL schemas across multiple backend services stitched together through a gateway (Apollo Federation being the most common implementation). This introduces an additional testing dimension: authorization logic that’s correctly enforced in one subgraph service can be silently bypassed if the gateway itself doesn’t consistently propagate authentication context to every downstream service it queries. I test this by crafting queries that span multiple subgraphs in a single request and checking whether authorization is enforced uniformly across the resulting federated response, or whether one subgraph trusts the gateway’s forwarded request without independently verifying the caller’s actual permissions — a gap that’s easy to introduce when different teams own different subgraphs and make different assumptions about where authorization “should” happen.
Frequently Asked Questions
Is GraphQL inherently less secure than REST? Not inherently — it’s a different paradigm with different risks. Flexible query shape introduces resource exhaustion concerns REST doesn’t have in the same way, while field-level authorization complexity introduces access control risks that are easy to overlook.
Should introspection always be disabled in production? It’s a reasonable hardening step and removes an easy reconnaissance path, but it shouldn’t be treated as a security control on its own — proper authorization and rate limiting matter far more.
What’s the best tool for GraphQL schema exploration? InQL (as a Burp extension) is my go-to for interactive testing, while GraphQL Voyager is excellent for visualizing type relationships once you have the schema.
Can GraphQL APIs be vulnerable to SQL injection? Yes — GraphQL is just a query layer sitting in front of whatever backend logic resolves each field; if that logic constructs SQL unsafely from arguments, the same injection risk applies as in any REST API.
How does rate limiting work differently for GraphQL? Simple per-request rate limiting is often insufficient because a single GraphQL request can contain many aliased or deeply nested sub-queries; proper GraphQL rate limiting needs to account for computed query cost or complexity, not just request count.
What is DVGA and is it legal to test against? Damn Vulnerable GraphQL Application is an open-source, intentionally vulnerable application built specifically for learning GraphQL security testing — running it locally in your own lab is completely legal.
Do persisted queries fully prevent GraphQL attacks? They significantly reduce risk by restricting clients to a pre-approved set of queries, closing off ad-hoc introspection-driven attacks, but they don’t replace the need for proper field-level authorization within those approved queries.
Conclusion
GraphQL penetration testing requires unlearning some REST-centric assumptions and building a new mental model centered on schema-aware exploration, resolver-level authorization, and query complexity risk. The core building blocks — introspection abuse, nested access control gaps, IDOR through arguments, and denial-of-service through query shape — cover the majority of real-world GraphQL vulnerabilities I encounter, and each has purpose-built tooling (InQL, clairvoyance, DVGA) that makes learning them hands-on and practical. Work through each vulnerability class deliberately in a legal lab, and GraphQL’s flexibility will start looking less like an unfamiliar attack surface and more like a structured set of testable assumptions.
For related API security methodology, see my guides on JWT security testing and broken access control testing.
References
- OWASP GraphQL Cheat Sheet
- Damn Vulnerable GraphQL Application (DVGA), official documentation
- GraphQL Foundation, GraphQL specification