GraphQL Security Testing: Introspection, Injection, and Authorization Flaws
GraphQL has grown from a Facebook internal tool to the API layer of choice for modern web applications. Its flexibility is the point — clients request exactly the data they need, in a single query, without multiple roundtrips. But that same flexibility creates attack surface that REST APIs simply don't have.
When you swap your REST endpoints for a single /graphql endpoint, you don't just change your API architecture. You change your attack surface, your authorization model requirements, and what your security tests need to cover.
This guide covers the GraphQL-specific vulnerabilities pentesters look for — and how to test for them systematically.
Step 1: Reconnaissance via Introspection
GraphQL has a built-in feature called introspection: the ability to query the schema itself to discover all types, fields, queries, and mutations the API supports. In development, this is invaluable. In production, it's a detailed roadmap for attackers.
An attacker's first move against any GraphQL endpoint is to run the full introspection query:
query IntrospectionQuery {
__schema {
queryType { name }
mutationType { name }
subscriptionType { name }
types {
...FullType
}
}
}
fragment FullType on __Type {
kind
name
description
fields(includeDeprecated: true) {
name
description
args { ...InputValue }
type { ...TypeRef }
isDeprecated
deprecationReason
}
inputFields { ...InputValue }
interfaces { ...TypeRef }
enumValues(includeDeprecated: true) { name description isDeprecated deprecationReason }
possibleTypes { ...TypeRef }
}
fragment InputValue on __InputValue {
name
description
type { ...TypeRef }
defaultValue
}
fragment TypeRef on __Type {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType { kind name ofType { kind name ofType { kind name ofType { kind name } } } }
}
}
}
If introspection is enabled in production, this query returns your complete API schema — every type, every field, every mutation, including ones that aren't in your documentation or frontend.
What pentesters look for in the schema:
- Admin or internal fields not exposed in the frontend (e.g.,
deleteUser,setRole,resetAllPasswords) - Fields marked as deprecated — often still active and under-scrutinized for security
- Type relationships that reveal authorization boundaries to probe
- Mutation names that suggest privileged operations
Even without introspection, attackers have options: GraphQL Clairvoyance and similar tools can reconstruct schemas by sending words from dictionary lists and observing error messages. If the API suggests corrections ("Did you mean adminUsers?"), the schema is partially discoverable.
Testing introspection status
# Quick check — if this returns schema data, introspection is enabled
curl -s -X POST https://target.com/graphql \
-H 'Content-Type: application/json' \
-d '{"query":"{__schema{types{name}}}"}'
Remediation: Disable introspection in production. Most GraphQL libraries support this with a single configuration flag. Provide schema documentation through a separate, access-controlled documentation system instead.
GraphQL Injection
GraphQL doesn't eliminate injection — it changes where and how it manifests. When GraphQL arguments are passed unsanitized to backend data stores, the traditional injection vulnerabilities appear.
SQL injection through GraphQL arguments
If a resolver passes query arguments directly to a database query without parameterization:
query {
users(filter: "name = 'admin'--") {
id
email
passwordHash
}
}
The filter argument lands in a SQL query. Classic SQL injection techniques apply: UNION selects, time-based blind injection, error-based extraction.
What to test:
- All string arguments in queries and mutations — try
',",; --,OR 1=1 - Filter and search arguments particularly — these are most likely to feed directly into queries
- JSON blob arguments — some implementations parse JSON values as dynamic query fragments
NoSQL injection (MongoDB)
Applications using MongoDB as a GraphQL backend are susceptible to NoSQL injection through operator injection:
query {
login(username: {"$gt": ""}, password: {"$gt": ""}) {
token
user { id role }
}
}
The MongoDB $gt operator with an empty string matches any non-null value — effectively bypassing authentication entirely.
Server-Side Request Forgery via GraphQL
GraphQL mutations that accept URLs (webhook registration, avatar upload from URL, integration configuration) are a vehicle for SSRF. The server fetches the URL you provide:
mutation {
setWebhook(url: "http://169.254.169.254/latest/meta-data/iam/security-credentials/") {
success
}
}
If the server returns the response of the fetched URL, you've achieved SSRF with data exfiltration. Even blind SSRF (no response returned) can probe internal network topology.
Authorization in GraphQL: Where It Goes Wrong
Authorization is GraphQL's most consistently misunderstood security concern. In REST, you put authorization checks in the endpoint handler — one handler, one authorization check. In GraphQL, a single query can traverse multiple types, each potentially owned by a different resolver. Each resolver is responsible for its own authorization, and it's easy to miss one.
Field-level authorization gaps
Consider a schema where User has both public fields (username, bio) and private fields (email, phoneNumber). If authorization is checked at the query resolver level but not at the individual field resolver level:
query {
users {
username
email # should be private
phoneNumber # should be private
}
}
An unauthenticated or low-privilege user enumerates all users with private contact information.
What pentesters test:
- Query every field on every user-related type as a low-privilege user
- Compare response with expected visible fields
- Look for fields that return data when they should return null or an error
IDOR via GraphQL (Broken Object Level Authorization)
The BOLA/IDOR problem exists in GraphQL the same way it exists in REST. If an ID argument isn't validated against the requesting user's ownership:
query {
order(id: "ord_9999") { # this belongs to another user
items { name price }
paymentMethod { last4 type }
shippingAddress { street city }
}
}
Getting another user's order details, including payment and address data, is a significant privacy and compliance violation.
Mutation authorization gaps
Mutations often get less security testing than queries because they're less visible in normal application flow. Pentesters specifically probe:
- Admin mutations reachable from standard user tokens
- Mutations that accept IDs without ownership verification
- Soft-delete mutations — can a user delete another user's data?
- Status-change mutations — can a user change the state of a resource they don't own?
Batching Attacks and Rate Limit Bypass
GraphQL's batching capability — the ability to send multiple queries in a single HTTP request — is a legitimate performance feature that attackers repurpose for brute-force and enumeration.
Query batching for credential brute-force
Standard rate limiting operates at the HTTP request level. If the API accepts batched queries:
[
{"query": "mutation { login(email: \"admin@company.com\", password: \"password1\") { token } }"},
{"query": "mutation { login(email: \"admin@company.com\", password: \"password2\") { token } }"},
{"query": "mutation { login(email: \"admin@company.com\", password: \"password3\") { token } }"},
... (999 more)
]
One HTTP request carries 1,000 login attempts. Rate limiting fires based on request count, not attempt count. You've effectively bypassed it.
Alias-based batching
Even without array batching, GraphQL aliases allow multiple calls to the same resolver in one request:
query {
a1: login(email: "admin@company.com", password: "password1") { token }
a2: login(email: "admin@company.com", password: "password2") { token }
a3: login(email: "admin@company.com", password: "password3") { token }
}
Same effect — rate limiting by HTTP request count is bypassed.
Field duplication / amplification
query {
user(id: "1") {
friend { friend { friend { friend { friend { friend {
id name email posts { title body comments { body author { email } } }
}}}}}}
}
}
Deep nesting can cause exponential resolver calls, consuming backend resources disproportionate to the request size. This is sometimes called a "GraphQL bomb."
Remediation: Implement query depth limits, query complexity analysis, and request-level rate limiting that accounts for operation count within batched requests.
Information Disclosure via Error Messages
GraphQL APIs, especially in development mode or with verbose error handling, often return detailed error messages that reveal internal implementation details:
{
"errors": [{
"message": "Cannot query field 'adminToken' on type 'User'",
"extensions": {
"code": "GRAPHQL_VALIDATION_FAILED",
"stacktrace": [
"GraphQLError: ...",
"at resolveField (/app/node_modules/graphql/execution/execute.js:461:18)"
]
}
}]
}
Stack traces reveal framework versions, file paths, and internal architecture. Suggestion errors reveal field names. Even suppressed stack traces may appear in server logs that are incorrectly exposed.
What pentesters probe:
- Malformed queries — missing required arguments, wrong types, invalid syntax
- Queries for non-existent fields — does the server suggest similar fields?
- Deeply nested objects — does resolver error handling expose internal paths?
Subscription Security
GraphQL subscriptions — real-time updates over WebSocket — introduce additional attack surface that's often overlooked in security reviews:
- Authorization on subscription events: Does a subscription for order updates filter events to the subscribing user, or can one user subscribe and receive all users' order events?
- WebSocket authentication: Is the WebSocket connection authenticated with the same token validation as HTTP requests, or with a weaker scheme?
- Subscription persistence: If a user's permissions change, are their active subscriptions revoked, or do they continue receiving data they're no longer authorized to see?
- Subscription-based DoS: Can an attacker open unlimited subscriptions to exhaust connection resources?
GraphQL Security Testing Toolkit
| Tool | Use Case |
|---|---|
| GraphQL Voyager | Visualize introspection results as an interactive graph |
| InQL (Burp Suite plugin) | Automatic query generation from introspection, security testing |
| GraphQL Clairvoyance | Schema reconstruction when introspection is disabled |
| Altair GraphQL Client | Manual exploration with authentication support |
| graphql-cop | Automated GraphQL security audit (DoS, introspection, batching) |
| nuclei GraphQL templates | Automated detection of introspection, common misconfigurations |
A GraphQL Security Testing Checklist
Reconnaissance
- Is introspection enabled? Run the full introspection query
- If disabled, attempt schema reconstruction with GraphQL Clairvoyance
- Map all types, fields, queries, mutations, subscriptions
- Identify admin/privileged operations in the schema
Authorization
- Test every field on user-related types as a low-privilege user
- Test every mutation with IDs that belong to a different user
- Test admin mutations with standard user tokens
- Test subscriptions for data isolation between users
Injection
- Test all string arguments for SQL injection (
',",; --) - Test all object arguments for NoSQL operator injection (
$gt,$ne,$regex) - Test URL-accepting fields for SSRF
Rate limiting and DoS
- Test batched query arrays for rate limit bypass on auth endpoints
- Test alias-based batching on the same resolver
- Test deeply nested queries for exponential resolver calls
- Test circular fragment references for infinite loop potential
Information disclosure
- Capture all error messages for stack traces, paths, or schema hints
- Test malformed queries for suggestion-based field enumeration
Ironimo's scanner includes nuclei templates for GraphQL security misconfigurations — introspection detection, batching attack surface identification, and common header/configuration issues. For GraphQL authorization testing (BOLA, field-level access), pair Ironimo's automated baseline with manual testing using the checklist above.
Start free scan