GraphQL Security Testing: Introspection, Injection, and Authorization Flaws
GraphQL changes the shape of an API, and it changes the shape of the attack surface. A single endpoint that accepts arbitrary queries from the client is a different threat model than a collection of REST endpoints with discrete inputs. Most security teams are still testing GraphQL like it is REST — checking authentication on the endpoint and calling it done. That misses most of what actually matters.
This guide covers the GraphQL-specific attack surface: what to enumerate, what to inject, how authorization breaks at scale, and what query complexity abuse looks like in practice.
Step 1: Introspection as Reconnaissance
GraphQL introspection lets clients query the API schema — every type, field, argument, mutation, and subscription. In production APIs this is frequently left enabled, giving attackers a complete map of the API before they send a single real request.
The introspection query:
query IntrospectionQuery {
__schema {
queryType { name }
mutationType { name }
types {
name
kind
fields {
name
args { name type { name kind ofType { name kind } } }
type { name kind ofType { name kind } }
}
}
}
}
Run this first. If the API returns a schema, you have enumerated every accessible operation. Tools like GraphQL Voyager, InQL (Burp extension), and clairvoyance (for introspection-disabled endpoints) automate this step.
When introspection is disabled, attackers use field suggestion attacks. GraphQL often returns suggestions when a field name is close to a valid one:
{ user(id: 1) { passwrd } }
# Response:
# "Cannot query field 'passwrd'. Did you mean 'password'?"
Tools like clairvoyance systematically probe with common field names and build a schema from suggestions alone. Disabling introspection is not a sufficient control.
Step 2: Authorization Testing in GraphQL
REST API authorization is relatively easy to test: does endpoint X return data for the wrong user? GraphQL authorization is harder because the same resolver may be called from multiple query paths, and authorization logic is frequently scattered across individual resolvers rather than applied at a gateway.
Horizontal privilege escalation via direct object reference
The most common finding. GraphQL resolvers often accept arbitrary IDs and return objects without checking ownership:
# Authenticated as user 123, requesting another user's data
query {
user(id: 456) {
email
phone
paymentMethods { last4 cardType }
orders { id total items { name price } }
}
}
Test every query that accepts an ID argument with out-of-scope IDs. Enumerate using sequential integers and GUIDs from other accounts you control.
Nested query authorization bypass
Authorization checks on the top-level query may not propagate to nested resolvers. A common pattern:
# Direct access to private resource is blocked
query { adminPanel { users { email password_hash } } }
# But the same data is reachable through a less-protected parent
query {
publicProduct(id: 1) {
reviews {
author {
email
adminPanel { users { email password_hash } }
}
}
}
}
Map the type graph from introspection, then trace paths to sensitive fields through parent types that have weaker authorization. Every path to a sensitive type is a potential bypass.
Mutation authorization
Mutations modify state and are higher impact. Test whether mutations enforce authorization independently of queries:
- Delete or modify another user's resources
- Elevate role or permission fields
- Access admin mutations without admin role
- Bypass field-level authorization on sensitive inputs
Step 3: Injection via GraphQL Arguments
GraphQL arguments are passed to resolvers, and resolvers frequently build database queries or call backend services with those arguments. The injection surface is the same as REST — SQL, NoSQL, SSRF, command injection — but the delivery mechanism differs.
SQL injection in GraphQL resolvers
# Standard SQLi probe via argument
query {
users(filter: "' OR '1'='1") {
id email
}
}
# Order-by injection (common in list queries)
query {
products(orderBy: "name; DROP TABLE products--") {
id name price
}
}
Use sqlmap with the --data flag pointing at a captured GraphQL POST request. For JSON-encoded queries, set the content type and point sqlmap at the variable under test. Alternatively, use gqlmap which understands GraphQL query structure natively.
NoSQL injection
GraphQL over MongoDB is particularly susceptible. MongoDB operators in arguments that get passed to find() queries without sanitization:
query {
user(email: { "$gt": "" }) {
id email password_hash
}
}
SSRF via URL-type arguments
mutation {
importData(sourceUrl: "http://169.254.169.254/latest/meta-data/") {
status
data
}
}
Check any argument that accepts a URL, hostname, or path for SSRF — webhook URLs, import sources, avatar URLs, redirect targets.
Step 4: Query Complexity and DoS
GraphQL's flexible query model enables clients to request deeply nested data in a single query. Without server-side controls, a small client request can produce a massive server-side workload.
Deeply nested queries
query {
user(id: 1) {
friends {
friends {
friends {
friends {
friends {
id email
}
}
}
}
}
}
}
Five levels of friends on a social graph triggers exponential database queries. Test with progressively deeper nesting and measure response time and server load. Well-configured APIs reject queries exceeding a complexity threshold or depth limit.
Field duplication / alias attacks
query {
a1: expensiveOperation(input: "x") { result }
a2: expensiveOperation(input: "x") { result }
a3: expensiveOperation(input: "x") { result }
# ... repeat 500 times
}
GraphQL aliases allow the same field to be queried multiple times in one request. Without alias deduplication, each alias resolves independently. This is a common application-layer DoS vector.
Batching attacks
GraphQL supports request batching — sending an array of operations in a single HTTP request. This bypasses rate limits that operate at the HTTP request level:
[
{ "query": "mutation { login(username: \"admin\", password: \"pass1\") { token } }" },
{ "query": "mutation { login(username: \"admin\", password: \"pass2\") { token } }" },
{ "query": "mutation { login(username: \"admin\", password: \"pass3\") { token } }" }
]
Send 1,000 login attempts in a single HTTP request. One request, one rate limit token consumed, 1,000 password guesses executed. This is how batching attacks bypass brute-force protections on authentication endpoints. Test any mutation that accepts credentials, OTPs, or verification codes.
Step 5: Subscription Security
GraphQL subscriptions maintain persistent WebSocket connections that push data to the client when events occur. They introduce a different attack surface:
- Authorization on subscription events — verify the server re-validates authorization when sending subscription events, not only at connection time
- Event leakage — check whether subscription events include data for other users or resources the subscriber should not see
- Connection exhaustion — opening large numbers of subscription connections without per-client limits can exhaust server resources
- Insecure upgrade path — WebSocket connections may not inherit session/token validation from the HTTP upgrade request
subscription {
orderUpdated(userId: 456) { # test with another user's ID
id status total
}
}
Common GraphQL Security Findings
| Finding | Severity | Common cause |
|---|---|---|
| Introspection enabled in production | Medium | Default framework configuration |
| IDOR via argument-level access control | High | Resolver checks missing or incomplete |
| Authorization bypass via nested types | High | Auth logic at query level, not field level |
| SQL injection via unsanitized arguments | Critical | String concatenation in resolver queries |
| Batching attack bypasses rate limiting | High | No per-operation rate limiting |
| No query depth or complexity limits | Medium | Framework defaults, no custom middleware |
| Verbose error messages with stack traces | Low/Medium | Debug mode enabled in production |
Testing Tools
- InQL — Burp Suite extension for GraphQL schema extraction, mutation testing, and batch attack generation
- GraphQL Voyager — visual schema explorer from introspection data
- clairvoyance — schema recovery via field suggestion when introspection is disabled
- gqlmap — automated injection testing for GraphQL arguments
- Altair / GraphiQL — interactive query development for manual testing
- Burp Suite — intercept and modify GraphQL requests; use match-and-replace for batch generation
What Automated Scanners Miss
Standard DAST scanners treat GraphQL endpoints as black boxes and typically miss:
- Schema enumeration via field suggestions (introspection disabled scenarios)
- Authorization bypass through nested type traversal — requires understanding the type graph
- Batch attack viability — requires generating structured batched requests
- Application-logic IDOR — requires test accounts in different privilege levels
- Subscription authorization — requires WebSocket-aware testing tools
Effective GraphQL security testing combines automated schema extraction with manual authorization verification across all query paths. The schema is the map; the authorization logic is what you are actually testing.
Testing an API-heavy application? Ironimo runs authenticated scans against GraphQL endpoints — covering injection, introspection exposure, and common authorization patterns — using the same Kali Linux toolchain a professional pentester would use.
Start free scan