Security Concepts10 min read11 October 2026

GraphQL Security: Vulnerabilities, Attacks and Defences | Yrzo AI

GraphQL APIs introduce unique security risks including introspection abuse, query batching attacks, and field-level authorisation failures. Learn how to test and secure them.

By Yrzo AI — UK cybersecurity specialists

GraphQL Security: What Makes It Different

GraphQL is a query language for APIs that lets clients specify exactly the data they need, fetch multiple resources in a single request, and navigate relationships between objects — all from a single endpoint. It was designed to solve real problems with REST: over-fetching, under-fetching, and proliferating endpoints. It solves those problems well. But the same flexibility that makes GraphQL powerful also introduces a class of security vulnerabilities that REST APIs do not have.

The shift to GraphQL has been rapid across UK tech companies, startups, and enterprise platforms alike. Many of those deployments have landed in production with default configurations that expose introspection data, no query depth limits, and authorisation logic bolted on as an afterthought. This article covers the major GraphQL attack surface areas and how to address them.

Introspection: The Attacker's Map

GraphQL supports introspection — a built-in feature that lets clients query the schema itself, discovering every type, field, query, mutation, and argument the API exposes. This is invaluable for developers exploring an API. It is equally invaluable for attackers mapping an application they are targeting.

A simple introspection query:

```graphql { __schema { types { name fields { name type { name } } } } } ```

Submit this to a GraphQL endpoint and, if introspection is enabled, you receive the complete schema — every query, mutation, type, and field the server supports. An attacker now knows exactly what operations exist, what data they return, and what parameters they accept. Tools like GraphQL Voyager and InQL visualise this data into a navigable schema map within seconds.

**Remediation:** Disable introspection in production. Most GraphQL libraries have a one-line configuration option:

```javascript // Apollo Server const server = new ApolloServer({ schema, introspection: process.env.NODE_ENV !== "production", }); ```

Disabling introspection makes reconnaissance significantly harder, though determined attackers can still probe the schema field-by-field using brute force.

Query Depth and Complexity Attacks

GraphQL allows clients to construct deeply nested queries following object relationships. If a `User` type has a `friends` field that returns a list of `User` objects, each of which also has a `friends` field, a client can construct a query that recursively fetches friends-of-friends-of-friends to arbitrary depth:

```graphql { user(id: "1") { friends { friends { friends { friends { friends { name email } } } } } } } ```

A sufficiently deep or complex query can exhaust server memory, trigger massive database load, or time out the application — a Denial of Service attack using nothing more than a carefully crafted query. This requires no authentication.

**Remediation:**

```javascript import depthLimit from "graphql-depth-limit"; import { createComplexityLimitRule } from "graphql-validation-complexity";

const server = new ApolloServer({ schema, validationRules: [ depthLimit(10), createComplexityLimitRule(1000), ], }); ```

Set maximum query depth (typically 5–10 for most applications) and query complexity limits. Reject queries that exceed them before execution.

Batching and Rate Limit Bypass

GraphQL supports query batching — sending multiple operations in a single HTTP request:

```json [ {"query": "mutation { login(email: \"user@example.com\", password: \"pass1\") { token } }"}, {"query": "mutation { login(email: \"user@example.com\", password: \"pass2\") { token } }"}, {"query": "mutation { login(email: \"user@example.com\", password: \"pass3\") { token } }"} ] ```

Rate limiting applied at the HTTP request level counts this as one request, but the server executes all three login mutations. An attacker can effectively brute force authentication by batching thousands of password attempts into a small number of HTTP requests — bypassing IP-based rate limits entirely.

The same technique applies to OTP verification, password reset codes, and any other brute-forceable operation.

**Remediation:** Apply rate limiting at the operation level, not just the HTTP level. Disable query batching if your application does not require it. Apply query complexity limits that account for the number of distinct operations in a batch.

Field-Level Authorisation Failures (IDOR via GraphQL)

REST APIs often implement authorisation at the route level — a middleware checks permissions before the handler runs. GraphQL's single-endpoint architecture means authorisation must be implemented at the resolver level, for every field. When this is inconsistent, attackers access data they should not.

A common pattern — the user query correctly checks that the requester owns the requested user object, but the `adminNotes` field on the User type has no separate authorisation check:

```graphql { user(id: "victim-uuid") { name email adminNotes # returns data intended only for staff internalFlags # exposes internal account state } } ```

The resolver for `adminNotes` simply returns the database field without checking whether the requesting user has staff privileges.

**Remediation:** Authorisation belongs in resolvers, not just at the query entry point. Use a library like `graphql-shield` to define permission rules declaratively:

```javascript import { shield, rule, and } from "graphql-shield";

const isAuthenticated = rule()((parent, args, ctx) => ctx.user !== null); const isAdmin = rule()((parent, args, ctx) => ctx.user?.role === "admin");

const permissions = shield({ User: { adminNotes: and(isAuthenticated, isAdmin), internalFlags: and(isAuthenticated, isAdmin), }, }); ```

Mass Assignment via Mutations

GraphQL mutations can expose object fields that should not be directly settable by clients:

```graphql mutation { updateUser(input: { name: "Attacker" role: "admin" # should not be client-settable verified: true # should only be set by email verification flow subscriptionTier: "enterprise" # should only be set by payment flow }) { id role } } ```

If the mutation resolver passes the entire input object to an ORM update without filtering which fields clients are permitted to set, every field in the schema input type is writable.

**Remediation:** Define separate input types for client-facing mutations that include only the fields clients are permitted to modify. Never pass raw client input directly to ORM update operations.

Injection via GraphQL Arguments

GraphQL arguments are often passed to database queries. Without proper parameterisation or sanitisation, they are vulnerable to the same injection attacks as any other user input:

```graphql { users(filter: "' OR '1'='1") { id email passwordHash } } ```

NoSQL databases used behind GraphQL APIs (MongoDB, DynamoDB) are vulnerable to operator injection:

```json { "query": "{ users(id: { \"$gt\": \"\")) { email } }" } ```

**Remediation:** Use parameterised queries or ORM methods that separate data from query structure. Validate and sanitise all argument values before use in database operations.

Information Disclosure via Error Messages

GraphQL's default error handling returns detailed error messages including stack traces, database error strings, and internal object identifiers. In production, these responses should be sanitised:

```json { "errors": [{ "message": "Error: connect ECONNREFUSED 10.0.1.45:5432 — PostgreSQL connection failed", "locations": [{"line": 2, "column": 3}], "path": ["user"], "extensions": { "code": "INTERNAL_SERVER_ERROR", "exception": { "stacktrace": ["Error: connect ECONNREFUSED...", "at TCPConnectWrap..."] } } }] } ```

This reveals the internal IP address of the database server, the database engine and version, and the internal network topology.

**Remediation:** In production, mask internal error details and return only a generic error message and a correlation ID for internal logging.

Persisted Queries

Persisted queries — where clients send a hash identifier for a pre-approved query rather than the full query string — significantly reduce the attack surface of a GraphQL API. Attackers cannot send arbitrary queries; they can only use hashes the server recognises. This prevents introspection, query depth attacks, batching abuse, and injection via novel query structures.

Implementing persisted queries requires more development effort but provides substantially stronger security for applications where the client and server are both under your control.

How Yrzo AI Tests GraphQL APIs

Yrzo AI's scanner detects GraphQL endpoints, probes for introspection, tests query depth and complexity limits, attempts batch-based rate limit bypass, and probes for field-level authorisation failures across discoverable types. Every confirmed finding is delivered with the full request and response, the specific risk, and remediation steps mapped to common GraphQL libraries.

[Scan your GraphQL API at yrzoai.dev](https://yrzoai.dev) — results in 24 hours.

Find out if your website has these vulnerabilities

Yrzo AI runs 44 automated security checks and delivers a full report in under 20 minutes. Starting from £399.

Scan your website →