Back to Learning Center
highAPI SecurityCWE-200CWE-400CWE-863

GraphQL Security

GraphQL provides a powerful query language for APIs, but its flexibility introduces unique security challenges. Unlike REST APIs with fixed endpoints, GraphQL allows clients to define query structures dynamically, creating attack vectors that require specialized security measures to address.

Why GraphQL Security Is Different

GraphQL security testing must account for client-defined query structures, schema introspection, and the ability to traverse relationships dynamically. Attackers can combine fields, arguments, aliases, and batching to extract excessive data or stress backend systems in ways impossible with traditional REST APIs.

Introspection Attacks

GraphQL's introspection system allows clients to query the API's schema to discover available data types and their fields. Research shows 50% of GraphQL endpoints are targeted with introspection attacks. While convenient for development, this feature can be exploited to uncover sensitive schema details and plan sophisticated attacks.

Introspection Query

graphql
# Full schema introspection query
query IntrospectionQuery {
  __schema {
    types {
      name
      fields {
        name
        type {
          name
          kind
        }
        args {
          name
          type { name }
        }
      }
    }
    mutationType {
      fields { name }
    }
  }
}

# Discover all available queries
query {
  __schema {
    queryType {
      fields {
        name
        description
      }
    }
  }
}

Bypassing Disabled Introspection

Inserting special characters after the __schema keyword can bypass poorly implemented restrictions. This exploits common developer oversights in regex patterns. Spaces, newlines, and commas that GraphQL ignores may not be accounted for in blocklist patterns:

graphql
# Bypass attempts using special characters
query { __schema
{ types { name } } }

query { __schema,{ types { name } } }

query { __schema { types { name } } }  # Extra spaces

# Using suggestions when introspection is disabled
# Apollo GraphQL suggests query amendments in error messages
query { user { pasword } }  # Typo reveals "password" field exists

Query Batching Attacks

GraphQL supports query batching, sending multiple queries in one request. While the network sees a single request, the backend processes queries sequentially. This can lead to DoS attacks that bypass WAFs, RASP, and intrusion detection systems because abuse appears as normal traffic.

Batching DoS Attack

json
// Single HTTP request with multiple queries
[
  { "query": "{ users { id email passwordHash } }" },
  { "query": "{ users { id email passwordHash } }" },
  { "query": "{ users { id email passwordHash } }" },
  // ... repeated 1000 times
  { "query": "{ users { id email passwordHash } }" }
]

Brute Force via Batching

json
// Enumerate users in single request - bypasses rate limiting
[
  { "query": "{ user(id: 1) { email } }" },
  { "query": "{ user(id: 2) { email } }" },
  { "query": "{ user(id: 3) { email } }" },
  // ... enumerate all user IDs
  { "query": "{ user(id: 10000) { email } }" }
]

Alias-Based Attacks

Even when batching is disabled, attackers can use aliases to achieve similar effects. Aliases allow assigning different names to fields within a query, enabling multiple versions of the same query in a single request:

graphql
# Single query with aliases - bypasses batch limits
query {
  u1: user(id: 1) { email password }
  u2: user(id: 2) { email password }
  u3: user(id: 3) { email password }
  u4: user(id: 4) { email password }
  # ... hundreds of aliases
  u1000: user(id: 1000) { email password }
}

# Alias-based DoS
query {
  a1: expensiveQuery(input: "a")
  a2: expensiveQuery(input: "b")
  a3: expensiveQuery(input: "c")
  # Each alias triggers full query execution
}

Nested Query Attacks

GraphQL's ability to traverse relationships enables deeply nested queries that can overwhelm databases and cause exponential resource consumption:

graphql
# Deeply nested query causing exponential DB queries
query {
  users {
    friends {
      friends {
        friends {
          friends {
            friends {
              id
              email
              posts {
                comments {
                  author {
                    friends { id }
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}

Authorization Bypass

GraphQL's single endpoint model can lead to inconsistent authorization checks across different query paths to the same data:

graphql
# Direct user query - properly protected
query {
  user(id: 123) {
    email
    ssn  # Access denied
  }
}

# Indirect access via relationship - authorization bypass!
query {
  posts {
    author {
      email
      ssn  # Exposed through different path!
    }
  }
}

Prevention Strategies

Disable Introspection in Production

javascript
// Apollo Server - disable introspection
const server = new ApolloServer({
  typeDefs,
  resolvers,
  introspection: process.env.NODE_ENV !== 'production',
  playground: false,  // Disable GraphiQL in production
});

// Custom validation rule to block introspection
const NoIntrospection = (context) => ({
  Field(node) {
    if (node.name.value.startsWith('__')) {
      context.reportError(
        new GraphQLError('Introspection is disabled')
      );
    }
  }
});

Query Complexity Limits

javascript
import { createComplexityLimitRule } from 'graphql-validation-complexity';

const complexityLimitRule = createComplexityLimitRule(1000, {
  // Assign costs to operations
  scalarCost: 1,
  objectCost: 10,
  listFactor: 20,
  
  onCost: (cost) => {
    console.log(`Query cost: ${cost}`);
  },
});

const server = new ApolloServer({
  typeDefs,
  resolvers,
  validationRules: [complexityLimitRule],
});

Depth Limiting

javascript
import depthLimit from 'graphql-depth-limit';

const server = new ApolloServer({
  typeDefs,
  resolvers,
  validationRules: [
    depthLimit(5),  // Maximum query depth of 5
  ],
});

Rate Limiting and Batching Controls

javascript
// Limit batch size
const server = new ApolloServer({
  typeDefs,
  resolvers,
  // Limit operations per request
  allowBatchedHttpRequests: false,  // Disable batching entirely
  // Or limit batch size
  // maxBatchSize: 5,
});

// Alias limiting middleware
const aliasLimit = (maxAliases) => (next) => (root, args, context, info) => {
  const aliases = countAliases(info);
  if (aliases > maxAliases) {
    throw new Error(`Query exceeds max aliases (${maxAliases})`);
  }
  return next(root, args, context, info);
};

Field-Level Authorization

javascript
// graphql-shield for field-level permissions
import { shield, rule, and, or } from 'graphql-shield';

const isAuthenticated = rule()((parent, args, ctx) => {
  return ctx.user !== null;
});

const isAdmin = rule()((parent, args, ctx) => {
  return ctx.user.role === 'admin';
});

const isOwner = rule()((parent, args, ctx) => {
  return parent.userId === ctx.user.id;
});

const permissions = shield({
  Query: {
    users: isAdmin,
    user: isAuthenticated,
  },
  User: {
    email: or(isAdmin, isOwner),
    ssn: and(isAdmin, isOwner),
  },
});

Security Checklist

• Disable introspection in production environments

• Disable GraphiQL and similar schema exploration tools in production

• Implement query complexity limits with appropriate cost calculations

• Set maximum query depth limits (typically 5-10 levels)

• Limit or disable query batching entirely

• Restrict excessive use of aliases and directives

• Implement field-level authorization that checks all access paths

• Use persisted queries to allow only pre-approved operations

• Monitor query patterns for abuse and implement alerting

• Apply rate limiting per user/IP on both query count and complexity

Practice Challenges

View all