Security: Depth Limiting, Query Complexity and Introspection

Advanced
13 min

Security: Depth Limiting, Query Complexity and Introspection

Clients compose their own queries, so a single request can ask for a million rows or recurse through a relationship until the server runs out of memory. After this lesson you will be able to name the attacks that are specific to GraphQL, limit query depth and cost, and configure a production server so it exposes no more than it must.

GraphQL-Specific Threats

Standard web security still applies; on top of it, the query language opens its own attack surfaces:

| Threat | Example | Defence | |---|---|---| | Deep nesting | user { friends { friends { friends ... } } } on a cyclic schema | Depth limit | | Expensive selections | posts(first: 1000) { comments(first: 1000) { ... } } | Complexity or cost limit | | Alias and batch abuse | 500 aliased login attempts in one request | Alias limit, per-field rate limit | | Schema disclosure | Introspection reveals internal fields and error suggestions | Disable in production | | CSRF via simple requests | GET or text/plain mutations from another origin | Preflight enforcement |

Limiting Depth

Any schema with a cycle (User.posts and Post.author) can be nested indefinitely. The graphql-depth-limit package adds a validation rule that rejects documents above a chosen depth before execution starts:

javascript
import depthLimit from "graphql-depth-limit"; const server = new ApolloServer({ typeDefs, resolvers, validationRules: [depthLimit(8)], });

Pick the limit from real client operations plus a margin; most applications never exceed 8 to 10. The rule runs during validation, so a rejected query costs almost nothing.

Limiting Complexity

Depth alone misses wide queries. graphql-query-complexity assigns a cost to every field, multiplies lists by their first argument, and sums the total. With Apollo Server it runs in a plugin hook once variables are known:

javascript
import { getComplexity, simpleEstimator, fieldExtensionsEstimator } from "graphql-query-complexity"; const complexityPlugin = (schema, max = 1000) => ({ async requestDidStart() { return { async didResolveOperation({ request, document }) { const complexity = getComplexity({ schema, query: document, variables: request.variables, estimators: [fieldExtensionsEstimator(), simpleEstimator({ defaultComplexity: 1 })], }); if (complexity > max) { throw new GraphQLError(`Query too complex: ${complexity} > ${max}`, { extensions: { code: "QUERY_TOO_COMPLEX" } }); } }, }; }, });

fieldExtensionsEstimator reads per-field costs declared in the schema, such as complexity: ({ args, childComplexity }) => args.first * childComplexity on a list field; simpleEstimator supplies the default of 1. Log complexity for a while before enforcing a limit.

Hardening with GraphQL Armor

GraphQL Armor bundles depth, cost, alias, directive and token limits plus field-suggestion blocking into one plugin for Apollo Server and GraphQL Yoga:

javascript
import { ApolloArmor } from "@escape.tech/graphql-armor"; const armor = new ApolloArmor({ maxDepth: { n: 8 }, maxAliases: { n: 15 }, costLimit: { maxCost: 1000 } }); const server = new ApolloServer({ schema, ...armor.protect() });

It is the quickest way to cover the whole table above.

Production Configuration

  • Introspection. Apollo Server disables it when NODE_ENV is production; set introspection: false explicitly to be sure, and let tooling read the schema from the repository.
  • Field suggestions. "Did you mean passwordHash?" leaks names even with introspection off; Armor's blockFieldSuggestions removes them.
  • CSRF. Keep Apollo's csrfPrevention on and only accept application/json bodies so browsers must preflight cross-origin requests.
  • Limits. Cap request body size, set an execution timeout, and rate-limit sensitive mutations such as login per user or IP rather than per request.
Quick Quiz
Question 1 of 2

Why is a depth limit not sufficient on its own?

Key Takeaways

  • Client-composed queries create GraphQL-specific risks: deep nesting, wide selections, alias batching and schema disclosure.
  • graphql-depth-limit rejects deep documents during validation at almost no cost.
  • graphql-query-complexity scores queries by field cost and list size; enforce a limit after observing real traffic.
  • GraphQL Armor packages these protections with sensible defaults for Apollo Server and Yoga.
  • In production disable introspection, block field suggestions and keep CSRF prevention on.

Next lesson: Performance: Persisted Queries, APQ and Response Caching — reduce payload sizes, allow-list operations and cache responses safely.

Security: Depth Limiting, Query Complexity and Introspection - GraphQL | CodeYourCraft | CodeYourCraft