API & Web

GraphQL Security: Vulnerabilities and Best Practices

Query depth attacks introspection leaks field-level authorization and rate limiting for GraphQL APIs

GraphQL Security Fundamentals

GraphQL is a query language for APIs that gives clients precise control over what data they request. Unlike REST, where endpoints return fixed data shapes, GraphQL clients specify exactly which fields they need in a query. This flexibility introduces security challenges that do not exist in REST APIs.

GraphQL APIs share authentication and authorization requirements with REST APIs but add GraphQL-specific attack vectors:

  • Query depth attacks: Deeply nested queries that create exponential database load
  • Query complexity attacks: Wide queries requesting thousands of fields per request
  • Introspection leaks: The schema is self-describing — attackers can enumerate all types, fields, and relationships
  • Batching abuse: Multiple operations in a single request bypass per-request rate limiting
  • Field-level authorization failures: REST access control is per-endpoint; GraphQL access control must be per-field in each resolver

GraphQL is not inherently more or less secure than REST. Security depends entirely on implementation. Many GraphQL APIs ship with default configurations that are insecure in production.

Query Depth and Complexity Attacks

GraphQL's recursive schema allows deeply nested queries that create exponential database queries. A simple 6-level nested query on a social graph can trigger millions of database calls:

query {
  user(id: 1) {
    friends {
      friends {
        friends {
          friends {
            friends {
              name
            }
          }
        }
      }
    }
  }
}

Depth limiting

Vulnerable (no depth limit):

const server = new ApolloServer({ typeDefs, resolvers });
// No depth limit — attacker can nest queries arbitrarily deep

Secure (depth limited):

import depthLimit from 'graphql-depth-limit';

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

Complexity limiting

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

const server = new ApolloServer({
  validationRules: [
    createComplexityLimitRule(1000, {
      onCost: (cost) => console.log('Query cost:', cost),
      formatErrorMessage: (cost) =>
        `Query too complex (${cost}). Maximum allowed: 1000.`
    })
  ]
});

Introspection Leaks in Production

GraphQL introspection allows any client to query the complete schema — all types, fields, relationships, mutations, and subscriptions. In development, this powers tools like GraphQL Playground and Apollo Explorer. In production, it gives attackers a complete map of your API surface.

An attacker who discovers a GraphQL endpoint can enumerate the schema with a single query:

query {
  __schema {
    types {
      name
      fields {
        name
        type { name }
      }
    }
  }
}

The response reveals every data type, every field name, and every relationship — including fields that should not be accessible.

Disable introspection in production (Apollo Server):

const server = new ApolloServer({
  typeDefs,
  resolvers,
  introspection: process.env.NODE_ENV !== 'production'
});

Field-Level Authorization

In REST APIs, access control is applied at the endpoint level. In GraphQL, a single query can request fields from multiple types, and each field resolver runs independently. Authorization must be enforced in every resolver that accesses restricted data.

Vulnerable — missing field-level authorization:

const resolvers = {
  Query: {
    user: (_, { id }, context) => {
      if (!context.user) throw new AuthenticationError('Not authenticated');
      return db.users.findById(id);
    }
  },
  User: {
    email: (user) => user.email,   // Any authenticated user can read any email
    ssn: (user) => user.ssn,       // Any authenticated user can read any SSN
  }
};

Secure — resolver-level authorization:

const resolvers = {
  User: {
    email: (user, _, context) => {
      if (context.user.id !== user.id && context.user.role !== 'admin') {
        return null;
      }
      return user.email;
    },
    ssn: (user, _, context) => {
      if (context.user.role !== 'admin') {
        throw new ForbiddenError('Admin access required');
      }
      return user.ssn;
    }
  }
};

Libraries like graphql-shield provide a declarative permission layer:

import { shield, rule, and } from 'graphql-shield';

const isAuthenticated = rule()((parent, args, ctx) => ctx.user !== null);
const isAdmin = rule()((parent, args, ctx) => ctx.user?.role === 'admin');
const isOwner = rule()((user, args, ctx) => user.id === ctx.user?.id);

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

Batching Attacks and Rate Limiting

GraphQL supports sending multiple operations in a single HTTP request (batching). A rate limiter that counts HTTP requests will see one request while the server executes 100 operations — effectively bypassing per-request rate limits.

// Single HTTP request, 100 brute-force attempts
[
  { "query": "mutation { login(email: \"admin@example.com\", password: \"pass1\") { token } }" },
  { "query": "mutation { login(email: \"admin@example.com\", password: \"pass2\") { token } }" }
  // ... 98 more
]

Disable batching unless needed

const server = new ApolloServer({
  typeDefs,
  resolvers,
  allowBatchedHttpRequests: false // Default false in Apollo Server v4+
});

Rate limit by operation count

app.use('/graphql', (req, res, next) => {
  const body = req.body;
  const operationCount = Array.isArray(body) ? body.length : 1;

  if (operationCount > 10) {
    return res.status(429).json({ error: 'Too many operations in batch' });
  }
  next();
});

How CodeSlick Detects GraphQL Issues

CodeSlick's API security checks cover GraphQL-specific patterns in JavaScript and TypeScript:

  • Missing depth limiting: Apollo Server setup without depthLimit() validation rule
  • Introspection in production: introspection: true without an environment guard
  • Missing authentication middleware: GraphQL route handlers without authentication context validation
  • Batching without limits: GraphQL handlers that accept array bodies without operation count validation

All findings include OWASP API Top 10 mapping (API4: Unrestricted Resource Consumption for depth/complexity, API5: Broken Function Level Authorization for introspection) and AI-powered fix suggestions for Apollo Server and graphql-js setups.

Scan your GraphQL API code for authentication gaps, missing authorization, and input validation issues across 5 languages.

Frequently Asked Questions

Related Guides