GraphQL has replaced REST in many modern APIs because it lets the client describe exactly what data it needs in a single request, avoiding over fetching and simplifying the frontend development cycle. That same flexibility introduces specific attack vectors that escape the traditional web pentester used to the verb plus resource model of REST. When the client can compose its own query, declare aliases, nest sub selections and send chained mutations, the attack surface widens and controls that worked in REST stop applying in a trivial way.
This guide covers how to enumerate a GraphQL API, which vulnerabilities show up again and again in real engagements, what tooling offensive testers use and which technical defenses a platform team should implement to reduce risk without breaking the developer experience.
The essentials. GraphQL is not less secure than REST, but it requires different controls. The three families of bugs that show up most often during audits are introspection active in production, denial of service through nested or aliased queries and authorization failures at the resolver level. Disable introspection, query complexity limiter, persisted queries and per resolver authorization are the minimum baseline for any public GraphQL API.
GraphQL vs REST security differences
Three architectural traits of GraphQL change the way you audit and defend the API:
- Single endpoint. Every operation hits
/graphql(or an equivalent path). That simplifies routing but invalidates any WAF or rate limit configured by path. A rule that protects/api/usersdoes not protectquery { users { ... } }because the URL is always the same as far as the WAF is concerned. - Query language on the client. In REST the server decides what each endpoint returns. In GraphQL the client composes the query and the server resolves it. That means input validation is no longer "check the JSON body", it is "check the shape of the query AST".
- Batching and multi operation. A single HTTP request can carry multiple queries and mutations. Rate limiting per HTTP request is misleading, the relevant metric is how many effective operations actually execute.
- Inconsistent status codes. GraphQL returns almost everything as
200 OKwith anerrorsfield inside the body. Tools that inferred failure from the HTTP code (classic scanners, SRE dashboards) lose visibility and authorization errors slip through the logs unnoticed.
Discovery and enumeration
The first step in any GraphQL engagement is to locate the endpoint and rebuild the schema. The more complete the visible schema, the faster the attacker can progress.
- Introspection query. GraphQL ships with a native introspection mechanism that returns the full schema (types, fields, arguments, directives) when enabled. It is expected behavior in development, but most audits find introspection open in staging too and, more often than desirable, in production.
- Field suggestions. Even when introspection is disabled, many implementations return suggestions like
Did you mean "userId"?when the attacker tries an approximate field name. Iterating over common field dictionaries reconstructs a significant slice of the schema. - GraphQL Voyager, Playground and Apollo Sandbox. Exploration interfaces such as GraphQL Playground, GraphiQL, Voyager and Apollo Sandbox appear exposed in production with surprising frequency. When reachable they hand the attacker a full IDE over the API.
- Common paths. The usual paths are
/graphql,/api/graphql,/v1/graphql,/query,/gqland, on Hasura stacks,/v1/graphqlwith the console at/console. Targeted fuzzing finds the endpoint in minutes.
Most common GraphQL vulnerabilities
The following families show up in practically every audit of a mature GraphQL API. They are not exhaustive but cover most of the real risk.
- Introspection in production. Handing the full schema to the attacker drastically shortens discovery time and reveals administrative fields, internal mutations and legacy types that no legitimate client uses but remain in place.
- DoS via query depth. GraphQL allows recursive nesting of relationships. If the
UserandPosttypes reference each other, a query nested fifty levels deep triggers exponential SQL queries or exhausts process memory. - DoS via aliasing. Aliases let the client request the same field N times within the same operation under different names. An operation with two hundred aliases of the same expensive resolver multiplies the cost by two hundred without tripping per request rate limiting.
- DoS via batching. Implementations that support array batching accept multiple operations in a single HTTP request. Combined with aliasing and depth it allows building load bombs with one request.
- IDOR and BOLA in resolvers. The most frequent and highest impact bug. Authorization is checked at the gateway or in the authentication wrapper, but the resolver of a nested type does not verify that the current user has access to the referenced object. A mutation like
updateOrder(id: 12345)that accepts any id because the resolver trusts the input field. - Mass assignment in mutations. Mutations that accept a full input type and persist it without filtering sensitive fields. An
updateProfile(input: {name, email, role})that forgets to blockroleenables privilege escalation to admin. - Auth bypass through non propagation. The authentication check is applied to the root query but does not propagate to sub queries or fragments. Requesting a protected field as a child of a public one returns the data without token validation.
- Field suggestions leak. Same problem as during discovery, but it also leaks names of private fields and types during normal API use, helping to map hidden functionality.
- SSRF via mutation. Mutations that accept URLs as arguments (
importFromUrl,webhookTest,fetchAvatar) and fetch them from the backend without destination validation are a classic doorway to SSRF against cloud metadata services or internal networks. - NoSQL injection in resolvers. Resolvers that pass arguments straight to MongoDB, Elasticsearch or other document stores without sanitizing allow operators like
$ne,$regexor$whereand lead to authentication bypass or data extraction. - Subscription auth issues. GraphQL subscriptions travel over WebSockets with an initial
connectionInitmessage that carries the token. Implementations that only validate the token when the connection opens, and not on each message, allow subscription hijacking after expiration or reuse of connections after logout.
Tooling for GraphQL pentest
The standard tool chain combines interception, schema reconstruction, fuzzing and automated analysis.
- Burp Suite plus InQL. The InQL extension for Burp parses introspection, generates query templates for every type and lets you send mutations from Repeater with autocomplete. It is the foundation of manual testing.
- Clairvoyance. When introspection is disabled, Clairvoyance uses field suggestions to rebuild the schema iteratively against a name dictionary. It takes time but recovers most types and fields.
- GraphQL Cop. Automated check suite for insecure configurations (open introspection, alias overload, batching enabled, field suggestions, etc.) that works as a fast first pass.
- AutoGraphQL. A tool that, given an endpoint, automatically generates test queries and mutations, useful for initial fuzzing on large APIs.
- Apollo Studio. When an organization publishes its graph in Apollo Studio without restrictions, part of the internal documentation remains publicly accessible and becomes free intel for the red team.
- Hasura Console abuse. Hasura exposes an administrative console whose default authentication is an admin secret. Consoles reachable without a secret or with weak secrets hand over full backend control.
Illustrative case: nested query DoS
To illustrate the pattern without delivering a reusable payload, a query built on top of two types that reference each other can request fifty depth levels alternating user { posts { author { posts { author { ... } } } } }. The server expands the resolver at every level, fires a database query per node and consumes CPU and memory exponentially. Without a depth or complexity limiter, a single request that is legitimate from the protocol point of view exhausts the worker and, on small stacks, brings the service down. The defensive team notices the attack because the latency dashboard spikes, but the WAF does not see it because the request is syntactically valid.
Popular implementations and their pitfalls
Each implementation has its own catalog of defaults worth reviewing.
- Apollo Server. Introspection on by default in development mode. Automatic detection via
process.env.NODE_ENVfails when the variable is not set correctly and ends up active in production. Apollo Sandbox embedded at/graphqlby default. - GraphQL.js (reference). Does not include native depth or complexity limiters. You must install
graphql-depth-limitandgraphql-query-complexityexplicitly. - Hasura. Generates full CRUD mutations for every database table. Without configuring granular per role and per column permissions, any consumer that reaches the endpoint can read and modify entire tables.
- Postgraphile. Same pattern as Hasura, generates the schema from Postgres. Authorization delegates to Postgres RLS policies, which are permissive by default if not configured.
- AWS AppSync. Default authorization can be API key, IAM, Cognito or OIDC. Mixing authorization types in the same API without reviewing
@aws_authdirectives leaves resolvers reachable without a token. - Hot Chocolate (.NET). Broad support for federation and subscriptions. Subscriptions over WebSockets require explicit configuration of
connectionInitvalidation so the token is validated per message.
Technical defenses
The following countermeasures cover most of the attack families described and apply regardless of stack.
- Disable introspection in production. Unless there is an explicit need to expose the schema (cross domain federation, authenticated partners), introspection must be off in every publicly reachable environment. Keep a versioned schema dump in the repo for the internal team.
- Query depth limiter. Cap maximum nesting depth at a reasonable value for the graph (typically between five and ten levels). Libraries such as
graphql-depth-limitapply the check in the validation middleware. - Query complexity analysis. Assign a cost to each field and reject queries that exceed a total budget.
graphql-query-complexitylets you define static or dynamic costs (proportional to thefirstpagination argument, for instance). - Persisted queries (APQ). Instead of accepting any query from the client, the server keeps an allowlist of pre registered queries by hash. The client only sends the hash. This completely blocks ad hoc aliasing, depth and batching abuse, at the cost of a registration step during the build.
- Per field rate limiting. On top of per IP or per token rate limiting, applying limits to specific fields or expensive resolvers (full text search, export, PDF generation) prevents a client within quota from bringing down a heavy endpoint.
- Auth at resolver level. Do not trust the gateway check alone. Each type resolver must validate that the authenticated principal has access to the specific object it is resolving. Patterns like
dataloaderwith per batch owner checks are common. - Field level authorization. For sensitive fields inside an accessible type (e.g.
User.emailvisible only to the user itself or to an admin), apply per field authorization directives instead of filtering on the client. - Disable field suggestions in production. Most servers have an option to suppress suggestions in error messages. Turning it on in production reduces noise and leakage.
- Subscription auth plus connectionInit. Validate the token on
connectionInitand revalidate against revocation or expiration periodically during the lifetime of the connection. Do not assume that an open WebSocket is still authorized.
GraphQL and Apollo Federation
Apollo Federation lets you compose several subgraphs (services) into a supergraph exposed by a gateway. It brings clear operational benefits but adds its own attack surface.
- Federated gateway. The gateway composes queries and forwards sub queries to each subgraph. If authorization is enforced only at the gateway and the subgraphs trust the source, bypassing the gateway and hitting the subgraph directly is enough to evade controls.
- Supergraph security. The federated schema must be distributed in a controlled manner. Publishing the supergraph in accessible registries hands over a full map of the internal architecture.
- Subgraph isolation. Each subgraph must be reachable only from the gateway (network policies, mTLS, allowlist). If subgraphs live in an internal network but an attacker pivots from another compromised service, the damage amplifies because there is no extra authorization layer.
- Entities and
@requires. References between subgraphs (@key,@requires) introduce implicit resolvers that can bypass validation if not modeled carefully.
Frequently asked questions
Is GraphQL less secure than REST?
Not intrinsically. GraphQL shifts part of the responsibility from the server to the type model and the resolvers. Properly implemented, the bug catalog is comparable to REST. Poorly implemented, it multiplies impact because a single endpoint exposes the whole API.
Can introspection be left on in production?
Only when there is a clear use case (federation between authenticated partners, internal tooling behind VPN) and the schema does not expose sensitive information about business logic or administrative types. For public APIs the recommendation is to turn it off and keep the schema versioned in the repo.
Does a WAF protect a GraphQL API?
Only partially. Traditional WAFs based on path rules or SQLi patterns are blind to alias overload, depth attacks and batching. There are specific WAFs and proxies (Apollo Router, dedicated GraphQL security products) that parse the query AST and apply controls at that level.
Does persisting queries break the developer experience?
It changes the workflow but the tooling is mature. Apollo, Relay and other frameworks support APQ with automatic hash generation during the build. The frontend team develops with normal queries and the pipeline registers them before deployment.
Who should externally test a GraphQL API?
An external team with specific GraphQL experience, not a generic web pentest team. The tools, the methodology and the bug patterns are different. A pentester used only to REST takes longer and finds less.
Does the OWASP API Security Top 10 apply to GraphQL?
Yes, in its entirety. BOLA, broken authentication, mass assignment, security misconfiguration and unrestricted resource consumption have GraphQL specific manifestations but the conceptual category is the same. The Top 10 is the minimum coverage list.
Related resources
- API Penetration Testing (REST and GraphQL)
- What is Burp Suite and what is it for in web pentesting
- 5 Most Common Web Vulnerabilities in 2025
- What is SSRF (Server Side Request Forgery)
- Web Application Pentesting: complete guide
GraphQL audit with Secra
At Secra we run dedicated GraphQL API pentests that combine manual schema enumeration, targeted fuzzing on resolvers, field by field authorization validation and controlled denial of service tests. The methodology covers OWASP API Security Top 10 with specific adaptations for GraphQL (introspection, depth, aliasing, batching, federation) and delivers actionable implementation recommendations for the platform team.
If your organization exposes a public or partner facing GraphQL API and you need to validate that the baseline defenses (introspection, complexity, persisted queries, per resolver authorization) are properly implemented, you can contact Secra for a scoped proposal.
About the author
Secra Solutions team
Ethical hackers with OSCP, OSEP, OSWE, CRTO, CRTL and CARTE certifications, 7+ years of experience in offensive cybersecurity, and authors of CVE-2025-40652 and CVE-2023-3512.