Back to Technical Mode

GraphQL Introspection & Batching Abuse

GraphQL's introspection feature, left enabled in production, hands an attacker a complete, browsable map of every type, field, and mutation your API exposes — including ones never meant to be discovered. Beyond that, deeply nested or batched queries can force the server to do exponentially more work than a REST endpoint ever could from a single request.

Introspection is meant for development tooling (schema-aware editors, codegen) — leaving it enabled in production is equivalent to publishing your entire internal API reference to anyone who asks
A single GraphQL request can request the same expensive field many times, or nest queries many levels deep, multiplying the actual backend work far beyond what the request's size on the wire would suggest
Query depth limiting, query complexity/cost analysis, and disabling batched queries (or heavily rate-limiting them) are GraphQL-specific controls with no direct REST equivalent, because REST doesn't allow this kind of request-shape flexibility
Field-level authorization still applies in GraphQL exactly as it would in REST — a resolver that skips its own authorization check because 'the gateway already checked auth' is a common and serious gap

Want this flow explained in detail as it plays?

POST/graphql

Headers

Host:app.flowground.dev
Content-Type:application/json

Body

Loading...

Quick-Insert Payloads

Defense in depth

  • Disable introspection entirely in production environments — it should only ever be enabled in development/staging where the audience is trusted
  • Enforce a maximum query depth and a query complexity/cost budget (weighting expensive fields and connections higher) at the GraphQL gateway, rejecting any query that exceeds it before execution begins
  • Apply the same per-field authorization checks inside every resolver that you would apply to the equivalent REST endpoint — never assume an earlier gateway-level auth check covers every field a query might request

Vulnerable vs. Fixed

const server = new ApolloServer({
  typeDefs,
  resolvers,
  introspection: true, // enabled unconditionally, including in production
  // no depth limit, no complexity analysis configured
});