Axeploit
← Back to posts

GraphQL's Blind Spots: Why Introspection, Batching, and Nested Queries Are an Attacker's Playground

By Pallavi M

GraphQL was designed to solve real problems with REST APIs: over-fetching, under-fetching, multiple round trips for related data, and the difficulty of evolving APIs without versioning. It solved those problems well. The flexibility that makes GraphQL productive for frontend developers ask for exactly what you need, get exactly what you asked for is the same flexibility that creates security challenges that REST APIs handle differently or do not face at all.

The three blind spots in this post are not implementation bugs. They are features. Introspection is a feature. Batching is a feature. Deeply nested queries are enabled by the type system. Each of these features provides genuine value. Each of them, in a production environment without appropriate controls, provides genuine attack surface.

The distinction between "this is a vulnerability" and "this is a feature with security implications" matters for how you respond. You do not patch introspection out of the protocol. You make deliberate decisions about whether introspection should be enabled in production, what it should reveal if it is, and what controls are in place around the other features that interact with it. The security controls for GraphQL are configuration decisions and implementation patterns, not patches.

This post is about what those decisions are, why they matter, and what an attacker does when they are not in place.

Blind Spot One: Introspection as a Reconnaissance Tool

What Introspection Is

GraphQL introspection is a built-in query mechanism that allows clients to ask the server what schema it supports. A single introspection query returns the complete type system of the API: every type, every field, every argument, every relationship, every enum value. The response is a machine-readable blueprint of the entire API surface.

This is genuinely useful in development. Tools like GraphiQL, Postman's GraphQL support, and Apollo Studio use introspection to provide autocomplete, query validation, and documentation generation. Without introspection, building GraphQL tooling would be significantly harder.

In production, enabled without restriction, introspection is a complete reconnaissance gift to any attacker who reaches the endpoint.

What an Attacker Does With It

The attacker sends a single query:

The response is the complete schema. The attacker now knows:

  • Every type in the system including types like AdminUser, InternalAuditLog, PaymentMethod, StripeWebhookEvent that reveal the internal data model
  • Every field on every type including fields that are not exposed in the UI or documentation
  • Every mutation including mutations that create, modify, or delete data
  • Every argument to every field including arguments that might accept privilege escalation values

Tools built for GraphQL reconnaissance InQL, GraphQL Voyager, graphql-path-enum take the introspection response and produce visual schema maps, automatically generate query skeletons for every type, and enumerate all paths through the type graph. A skilled attacker with an introspection response and ten minutes has a more complete map of the API than most developers working on it.

What Happens When Introspection Reveals Hidden Types

The introspection response frequently reveals types and fields that are implemented but not surfaced in the application's UI or external documentation. These undocumented fields exist for several reasons: administrative interfaces that were implemented using the same GraphQL layer as the external API, deprecated fields that were removed from the frontend but not from the schema, internal metadata fields used by backend services, and fields that were added for testing and never cleaned up.

An attacker who finds a type called AdminActions with a field impersonateUser that accepts a userId argument has found something the development team may not have realized was queryable from the external endpoint. The field exists in the schema. The introspection query reveals it. Whether the resolver enforces authorization at the field level whether it checks that the caller has administrative privileges before executing the impersonation determines whether the exposed field is also an exploitable field.

The Controls

Disable introspection in production. Most major GraphQL server libraries provide a configuration option to disable introspection. This is the appropriate default for production endpoints exposed to untrusted clients. Introspection is a development tool. Production systems do not need to reveal their complete schema to the internet.

If introspection must remain enabled: Restrict it to authenticated sessions. Consider field-level filtering that removes internal or administrative types from the introspection response for non-administrative users. Log all introspection queries they are high-signal indicators of reconnaissance activity.

Implement field-level authorization regardless. Disabling introspection limits reconnaissance but does not protect against an attacker who already knows your schema (from source code, from documentation, from a prior introspection before you disabled it). Every field that should not be accessible to a given user must be protected by authorization logic at the resolver level, not only by the obscurity of the field name.

Blind Spot Two: Batching and Query Complexity Abuse

What Batching Is

GraphQL supports multiple operations in a single HTTP request through two mechanisms: aliases (requesting the same field multiple times with different arguments in a single query) and array batching (sending an array of operations in the request body). Both mechanisms allow the client to reduce round trips by combining multiple operations into a single request.

This is a meaningful performance optimization for legitimate clients. It is also a mechanism for amplifying the cost of a single HTTP request by orders of magnitude.

The Alias Rate Limit Bypass

Rate limiting in GraphQL is typically applied at the HTTP request level: a limit on how many requests per minute can be sent to the endpoint. An attacker who can send 1,000 operations in a single HTTP request bypasses a request-level rate limit entirely.

The alias technique:

One HTTP request. One thousand authentication attempts. A request-level rate limit that permits ten requests per minute allows ten thousand password attempts per minute.

The same technique applies to OTP brute force:

Ten thousand OTP codes in a single HTTP request. A six-digit OTP space exhausted in a handful of requests if the rate limiting is at the HTTP level rather than the operation level.

Array Batching

Some GraphQL servers support array batching sending an array of complete operation objects in the request body:

This is structurally different from alias batching but produces the same result: many operations in one HTTP request, bypassing HTTP-level rate limiting.

The Controls

Rate limit at the operation level, not the HTTP request level. Count each GraphQL operation (each alias, each batched operation) as a separate operation for rate limiting purposes. A request containing 1,000 aliases should consume 1,000 units against the rate limit, not 1.

Implement query complexity limits. Assign a cost to each field based on its computational and database load. Sum the cost of all fields in a query. Reject queries whose total cost exceeds a threshold. A query with 1,000 login aliases has a cost proportional to 1,000 login operations, not 1 HTTP request.

Disable array batching in production if it is not required by any legitimate client. Many GraphQL server libraries enable array batching by default. If your client applications do not use it, disabling it eliminates the array batching attack surface.

Implement per-operation rate limiting for sensitive mutations. Login, OTP verification, password reset, and similar authentication operations should have rate limits applied per operation rather than per request, keyed to the target account as described in the rate limiting post in this series.

Blind Spot Three: Deeply Nested Queries and Resolver Amplification

The N+1 Query Problem as a Security Issue

GraphQL's type system allows queries that traverse arbitrarily deep relationships:

Each level of this query potentially triggers additional database queries through the resolver chain. The users resolver queries the users table. The posts resolver for each user queries the posts table N times, once per user. The comments resolver queries the comments table N×M times. The depth multiplies the database load exponentially.

This is the N+1 query problem that GraphQL developers are familiar with and that DataLoader and similar tools partially address. In a security context, it is also a denial-of-service primitive: a single carefully constructed query that generates thousands or millions of database operations.

The query above, at a depth of 8 with reasonable data volumes, could generate millions of database queries in response to a single HTTP request. The server processes each resolver call. The database handles each query. The cost is O(n^depth) where n is the average result set size at each level.

Circular Reference Exploitation

GraphQL type systems can contain circular references types that reference each other. A User type might have a posts field that returns Post objects, and Post might have an author field that returns a User. A query can traverse this cycle indefinitely:

There is no schema-level constraint on this query's depth. The type system permits it. Whether the server handles it gracefully or processes an increasingly expensive resolver chain until it exhausts memory or times out depends on whether depth limits are implemented.

Authorization Bypass via Nested Queries

Beyond denial of service, deeply nested queries create authorization bypass opportunities. A field that is correctly protected at the top level may be accessible through a relationship path that bypasses the top-level authorization check.

Consider a GraphQL API where:

  • query { adminSettings { ... } } protected, requires admin role
  • query { user(id: "1") { organization { settings { ... } } } } each resolver checks only its immediate authorization

If the settings field on Organization has weaker authorization than adminSettings at the top level, a non-admin user can access administrative settings through the relationship path even though they cannot access them directly.

This authorization bypass through nested traversal appears consistently in GraphQL APIs that implement authorization at specific resolvers without considering the full set of paths through which each type can be reached.

The Controls

Implement query depth limits. Reject queries that exceed a maximum depth threshold. Depth 10 is a common starting point for most applications. Any query that requires depth greater than your legitimate application's deepest query is a candidate for rejection.

Implement query complexity analysis. Calculate the complexity of each query before executing it, using a scoring system that assigns costs to fields and multiplies costs through relationship traversals. Reject queries whose complexity exceeds a threshold. Libraries implementing this for major GraphQL servers exist for every major language ecosystem.

Use DataLoader to batch resolver calls. DataLoader prevents N+1 queries by batching resolver calls from the same query execution. This does not eliminate the denial-of-service risk from extremely deep queries but significantly reduces the multiplier effect at each level.

Implement field-level authorization consistently across all access paths. Every resolver for a field must enforce the authorization rules for that field regardless of which path the query took to reach it. The authorization check on settings.apiKeys must apply whether settings is accessed directly or through user.organization.settings. Authorization that is only applied at the entry point the top-level query fields does not protect fields that are reachable through nested traversals.

Apply timeouts to query execution. A query that takes longer than a threshold to execute should be terminated rather than allowed to continue consuming server resources. Query timeouts bound the worst-case resource consumption of any single request.

The GraphQL Security Testing Methodology

Testing a GraphQL endpoint for these three blind spots requires a different methodology than REST API testing. The single endpoint receives all queries there are no distinct URL paths to enumerate. The attack surface is the schema, not the route table.

Introspection reconnaissance: Send the introspection query. If it succeeds, analyze the response for sensitive type names, undocumented mutations, administrative operations, and fields that suggest privileged access patterns. Use InQL or a similar tool to automatically generate query skeletons for all types and mutations. Identify fields that could manipulate roles, access financial data, or perform administrative operations. Test each with minimum-privilege authentication to verify field-level authorization.

Alias batching test: Craft a query that uses aliases to duplicate a rate-limited operation 100 times. Send it. Observe whether the rate limiter fires. If 100 aliased operations in a single request succeed without rate limiting, scale to 1,000. Test specifically against login, OTP verification, and password reset mutations.

Depth and complexity test: Construct queries of increasing depth using circular references in the schema (identified via introspection). Send queries of depth 5, 10, 15, 20. Observe whether the server rejects high-depth queries or begins to show response time degradation consistent with exponential resolver amplification. A query that takes 10 seconds to respond at depth 15 is a denial-of-service vector regardless of whether the server ultimately errors.

Nested authorization test: For each sensitive type identified via introspection, identify every path through the schema that reaches that type. Test each path with minimum-privilege authentication. If the sensitive type is accessible via any relationship path that the direct query would have blocked, the authorization logic is inconsistent.

Why REST Security Assumptions Don't Transfer

Teams migrating from REST to GraphQL sometimes carry the assumption that their existing security tooling and mental models apply. They partially do. The fundamental principles authentication, authorization, input validation, rate limiting apply to GraphQL as they apply to any API. The specific implementation of those principles does not transfer cleanly.

Rate limiting. In REST, rate limiting per route is natural. Each endpoint has its own URL. In GraphQL, every request goes to the same endpoint. Rate limiting per URL is meaningless. Rate limiting per operation type requires understanding the GraphQL request body.

Authorization. In REST, authorization often happens at the route middleware level before the request reaches the handler. In GraphQL, the same route handles all operations. Route-level middleware cannot distinguish between a public query and a privileged mutation. Authorization must happen at the resolver level, per field, per operation.

Schema exposure. In REST, the API surface is exposed through routes what URLs exist, what methods they accept. In GraphQL, the entire schema is exposed through introspection in a format that is far more detailed and machine-readable than any route table. A disabled or restricted introspection endpoint is not equivalent to hiding REST routes it is hiding something that is architecturally more exposing.

Denial of service. REST endpoints can be expensive, but the cost of each endpoint is bounded and consistent. GraphQL queries can have unbounded cost due to nested resolver execution. A REST API with rate limiting per endpoint is protected from per-endpoint abuse. A GraphQL API with HTTP-level rate limiting and no query complexity analysis is not protected from query-level amplification.

Closing: The Schema Is a Contract. The Contract Has Attack Surface.

GraphQL's type system is one of its most powerful features. It makes APIs self-describing, enables powerful tooling, and provides a clean contract between frontend and backend. The same properties that make it powerful for developers make it legible for attackers.

The schema that introspection reveals is not just documentation. It is a map of every data type, every operation, every argument the API will accept. An attacker with that map and fifteen minutes with InQL has a more complete picture of the API than most members of the team that built it.

Batching is not just a performance feature. It is a mechanism to multiply the cost of a single request by orders of magnitude, bypassing the rate limiting that was designed at the wrong level of abstraction.

Deeply nested queries are not just an N+1 performance concern. They are denial-of-service primitives and authorization bypass vectors in APIs where field-level authorization is not consistently applied across all traversal paths.

None of this means GraphQL is the wrong choice. It means GraphQL security requires understanding the specific threat model that GraphQL's design introduces and making deliberate, informed decisions about introspection configuration, operation-level rate limiting, query complexity enforcement, and field-level authorization consistency.

The teams that make those decisions intentionally ship GraphQL APIs that are both powerful and defensible. The teams that carry REST security assumptions into GraphQL deployments ship powerful APIs with blind spots that an attacker will find before the security team does.

Axeploit's API security testing covers GraphQL specifically sending introspection queries to map the schema, constructing alias-batched attacks against authentication mutations, building deeply nested queries to identify complexity limits and authorization inconsistencies, and testing every sensitive type across all paths through the type graph that introspection reveals. Its autonomous agents understand GraphQL semantics rather than treating GraphQL endpoints as generic HTTP targets which is the difference between a security assessment that finds what matters and one that misses the blind spots entirely.

Integrate Axeploit into your workflow today!