Application Security

GraphQL Injection Prevention: Securing Your API Layer

GraphQL's flexible query language introduces injection risks that differ fundamentally from REST APIs. Preventing GraphQL injection requires understanding the query parser, resolver chain, and schema design.

Michael
Application Security Engineer
7 min read

GraphQL APIs handle query parsing differently than REST APIs handle URL routing, and this difference has security implications that many teams miss. With REST, the server defines rigid endpoints. With GraphQL, the client defines the shape of every request. This flexibility is GraphQL's greatest feature and its largest attack surface.

GraphQL injection is not a single vulnerability -- it is a category of attacks that exploit the query language, the resolver chain, or the schema definition to extract unauthorized data, cause denial of service, or bypass access controls. Defending against these attacks requires controls at multiple layers.

Query Complexity Attacks

The most accessible GraphQL attack is the complexity bomb. Because clients control query shape, a malicious client can construct deeply nested or widely fanned queries that force the server to perform exponential work.

Consider a schema where a User has friends, and each friend is also a User with their own friends. A query requesting friends { friends { friends { friends { ... } } } } nested 10 levels deep on a user with 100 friends could trigger 100^10 resolver calls. This is not a bug in your code -- it is an inherent property of graph-shaped data exposed through GraphQL.

Cost analysis middleware is the primary defense. Libraries like graphql-query-complexity or graphql-validation-complexity assign a cost to each field and reject queries that exceed a threshold before execution begins. The key is assigning accurate costs. A field that returns a scalar has a cost of 1. A field that returns a list has a cost proportional to the expected list size. A field that triggers an N+1 database query has a cost that reflects the actual server-side work.

Query depth limiting provides a blunt but effective backstop. Set a maximum depth (typically 7-12 levels depending on your schema) and reject deeper queries. This prevents the exponential nesting attack without requiring per-field cost analysis.

Request timeout enforcement is the last line of defense. If a query escapes complexity analysis (which can happen with custom directives or batched queries), a server-side timeout prevents it from consuming resources indefinitely.

Introspection Abuse

GraphQL introspection allows clients to query the schema itself -- discovering types, fields, arguments, and relationships. This is invaluable during development but provides attackers with a complete map of your API surface.

Disable introspection in production. This is a one-line configuration in most GraphQL servers (introspection: false in Apollo Server, for example). The argument that introspection is not a security mechanism because "security through obscurity is not security" misses the point. Introspection does not replace access controls, but it gives attackers reconnaissance information that makes exploiting other vulnerabilities faster.

If your API serves external developers who need schema documentation, provide it through a static schema file or documentation page rather than live introspection. This gives you control over what is documented and prevents schema changes from being immediately discoverable by adversaries.

Some introspection attacks are subtle. An attacker might use introspection to discover internal fields that exist in the schema but are not documented in your public API. Fields like _debug, internalNotes, or adminOverride that developers added for convenience but forgot to restrict.

SQL Injection Through Resolvers

GraphQL itself is not vulnerable to SQL injection. The GraphQL query parser handles GraphQL syntax, not SQL. But the resolvers that execute GraphQL queries often construct database queries, and this is where SQL injection enters the picture.

A resolver that takes a filter argument and interpolates it into a SQL query is vulnerable:

// DANGEROUS: SQL injection through GraphQL argument
const resolver = (_, args) => {
  return db.query(`SELECT * FROM users WHERE name = '${args.filter}'`);
};

The defense is the same as for any SQL injection: use parameterized queries or an ORM that handles parameterization. The GraphQL layer does not change the fundamental principle. But GraphQL's flexible argument system can make injection vectors less obvious because arguments are defined in the schema and may not receive the same scrutiny as REST endpoint parameters.

Custom scalar types can provide input validation at the schema level. Define a SafeString scalar that rejects input containing SQL metacharacters, or better yet, define domain-specific scalars (Email, UUID, Slug) that validate format before the resolver sees the input.

Authorization Bypass

GraphQL's graph-traversal nature creates authorization challenges that REST APIs rarely face. In REST, each endpoint can enforce its own authorization. In GraphQL, a single query can traverse multiple types, and each type needs its own authorization check.

Consider: a user is authorized to view their own profile but not others. In REST, the /users/me endpoint enforces this. In GraphQL, the user queries user(id: "other-user-id") { email, address, phone } -- and the resolver must verify authorization for both the user lookup and each requested field.

Field-level authorization is necessary in GraphQL schemas where different fields have different access requirements. A User type might expose name publicly, email to authenticated users, and socialSecurityNumber to administrators. Each field's resolver must check the requester's authorization.

Directive-based authorization provides a declarative way to enforce access controls. Schema directives like @auth(role: ADMIN) attached to fields or types declare the required authorization. A middleware layer evaluates these directives before resolvers execute.

The danger is assuming that if a user cannot see a field in the client application, they cannot query it. GraphQL clients can be modified to request any field the schema exposes. Server-side authorization is the only reliable control.

Batch and Alias Attacks

GraphQL allows multiple operations in a single request through batching and aliases. Both can be abused.

Batched queries send an array of operations in a single HTTP request. An attacker can batch thousands of mutations (password reset attempts, login attempts) in one request, bypassing rate limits that count HTTP requests rather than GraphQL operations.

Aliases allow the same field to be queried multiple times with different arguments in a single operation. An attacker can alias a login mutation 1000 times with different passwords, executing a brute-force attack in a single HTTP request.

Defend against both by implementing rate limiting at the operation level, not the HTTP request level. Count each operation in a batch and each aliased field separately against rate limits.

Persisted Queries

Persisted queries replace arbitrary client-defined queries with pre-registered query strings identified by hashes. The server maintains a whitelist of allowed queries and rejects anything not in the whitelist.

This is the strongest defense against query complexity attacks, injection, and introspection abuse because the attacker cannot send arbitrary queries -- they can only execute queries that your team has pre-approved. The tradeoff is development friction: every new query requires registration.

Automatic persisted queries (APQ) provide a middle ground. The client sends a query hash first, and if the server has seen and cached that query, it executes the cached version. If not, the client sends the full query, which the server caches for future requests. APQ provides performance benefits but weaker security because the server accepts arbitrary queries on first use.

Input Validation Patterns

GraphQL arguments should be validated at three layers:

Schema-level validation. Use non-nullable types, enums, and custom scalars to enforce structural constraints. A field that accepts a Status enum cannot receive an arbitrary string.

Resolver-level validation. Validate semantic constraints that the schema cannot express. A startDate must be before endDate. A quantity must be positive. An email must be properly formatted.

Persistence-level validation. Database constraints provide a final safety net. Unique constraints, foreign key constraints, and check constraints catch any input that escapes application-level validation.

How Safeguard.sh Helps

Safeguard.sh monitors the GraphQL server libraries and middleware in your dependency tree for known vulnerabilities. GraphQL frameworks like Apollo, Mercurius, and graphql-yoga have had security-relevant bugs in their query parsing and validation layers. Safeguard.sh flags these vulnerabilities in your SBOM and alerts your team before attackers can exploit them. For teams building GraphQL APIs, Safeguard.sh provides continuous supply chain monitoring for the framework code that sits between your network edge and your business logic.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.