>samit_hota
Back to research
ETHICAL HACKING

Offense and Defense in GraphQL: Introspection, Batching, and Depth Abuse

Samit Hota·
#graphql#api-security#application-security

GraphQL eliminates the over-fetching problem of traditional REST APIs, but its single-endpoint architecture shifts the burden of query execution entirely to the server engine. If you deploy a GraphQL gateway with out-of-the-box defaults, you are essentially exposing an unconstrained execution environment to the public internet.

Understanding how to audit and secure a GraphQL implementation requires looking at three distinct operational behaviors: schema discovery via introspection, rate-limit evasion via request batching/aliasing, and resource exhaustion via nested depth queries.

Mapping the Attack Surface via Introspection

By design, GraphQL allows clients to query the schema itself using the __schema meta-field. In a development environment, this powers auto-completion in IDEs like GraphiQL or Playground. In production, it provides a complete map of every type, field, argument, and internal deprecation notice.

Executing a standard introspection query reveals the entire underlying data model:

query IntrospectionQuery {
  __schema {
    queryType { name }
    mutationType { name }
    subscriptionType { name }
    types {
      name
      kind
      description
      fields {
        name
        type {
          name
          kind
          ofType { name kind }
        }
        args {
          name
          type { name kind }
        }
      }
    }
  }
}

When sent to an unprotected endpoint, the response returns the full structural blueprint of the API, including hidden administrative fields or staging mutations left behind by developers.

To disable introspection in production using Node.js and Apollo Server, explicitly set the introspection flag based on the environment context:

const { ApolloServer } = require('@apollo/server');

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

Disabling introspection removes the convenience of automatic documentation discovery, forcing an auditor to rely on fuzzing techniques or client-side JavaScript bundle analysis to infer fields.

Bypassing Rate Limits with Query Aliasing and Batching

Traditional Web Application Firewalls (WAFs) and API gateways enforce rate limiting by tracking HTTP request counts per IP address or authorization token. GraphQL invalidates this model because a single HTTP POST request can contain hundreds of distinct execution instructions.

There are two primary techniques used to condense multiple operations into a single HTTP payload:

1. Field Aliasing

GraphQL allows clients to rename returned fields using aliases. An attacker targeting an authentication mutation or a sensitive lookup field can duplicate the field multiple times within a single query wrapper:

query EvasionTechnique {
  attempt1: login(username: "admin", password: "password123") { token }
  attempt2: login(username: "admin", password: "password456") { token }
  attempt3: login(username: "admin", password: "password789") { token }
}

To the network gateway, this is 1 HTTP request. To the GraphQL execution engine, it represents 3 distinct function calls running against the backend data store.

2. HTTP Request Array Batching

Many GraphQL servers accept an array of query objects inside a single HTTP request body:

[
  {"query": "query { getUser(id: 1) { email } }"},
  {"query": "query { getUser(id: 2) { email } }"},
  {"query": "query { getUser(id: 3) { email } }"}
]

If the server framework resolves batched requests sequentially or concurrently without validating total array size, rate limits applied at the HTTP transport layer are effectively bypassed.

Denying Service through Deeply Nested Query Graph Flooding

GraphQL schemas often contain relational links that allow circular traversals. For instance, an Author has many Books, and each Book has an Author.

Without execution depth validation, a query can recursively nest these relationships to consume excessive CPU cycles and memory on the database layer:

query MaliciousDepthQuery {
  author(id: "1") {
    books {
      author {
        books {
          author {
            books {
              author {
                name
              }
            }
          }
        }
      }
    }
  }
}

An exponential expansion occurs at each relational tier. A query nested 10 levels deep can force the engine to resolve thousands of backend database calls, stalling the server process or triggering memory exhaustion.

Hardening GraphQL: Implementing Defenses That Stick

Securing a GraphQL API requires moving defense mechanisms from the HTTP transport layer into the GraphQL execution layer.

Implementing Query Depth Limits

To prevent depth exhaustion attacks, integrate a validation rule that analyzes the Abstract Syntax Tree (AST) before execution and rejects queries exceeding a predefined depth threshold.

Using the graphql-depth-limit package with Express and Apollo Server:

const depthLimit = require('graphql-depth-limit');
const { ApolloServer } = require('@apollo/server');

const server = new ApolloServer({
  typeDefs,
  resolvers,
  validationRules: [
    // Limit queries to a maximum depth of 4 levels
    depthLimit(4)
  ],
});

Implementing Query Cost and Complexity Analysis

Depth limiting alone does not prevent wide queries (e.g., requesting 100 aliased top-level fields). Query complexity calculation assigns a numeric cost to fields and multiplies cost based on pagination arguments.

Using graphql-validation-complexity:

const { createComplexityLimitRule } = require('graphql-validation-complexity');

const ComplexityLimitRule = createComplexityLimitRule(1000, {
  onCost: (cost) => console.log('Query complexity cost:', cost),
  formatErrorMessage: (cost) => 
    `Query exceeds maximum complexity cost of 1000 (actual: ${cost}).`,
});

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

By combining disabled introspection in production, field-level complexity analysis, and depth limiting, the server enforces strict bounds on execution resource usage regardless of how queries are packaged at the HTTP layer.

Want a second set of eyes on your security posture?

Let's talk about where your real exposure is.

Book an advisory call