A GraphQL API puts the shape of every request in the client's hands, so controls built for REST, such as per-endpoint rate limits, gateway route rules and top-level authorisation, stop matching what the server actually does. One GraphQL endpoint can carry a deeply nested query, a hundred aliased login attempts or a fan-out across a dozen federated services, and each must be limited and authorised inside the GraphQL layer or the business logic behind it. This guide goes beyond the GraphQL summary in the API Security Guide: it covers schema exposure and introspection, query depth, cost and batching limits, alias abuse, resolver authorisation, injection through arguments and directives, persisted queries and federation. It is for API platform owners, application security engineers and developers running GraphQL in production, and it describes attacks conceptually only.
Key takeaways
- Count operations, not HTTP requests. Batching and aliases put many operations in one request, so rate limits and brute-force protection must run on resolver executions and query cost.
- Bound every query before executing it. Depth, amount, breadth, batch size and cost limits are checked on the parsed document; OWASP ASVS 4.3.1 requires at least one of an allowlist, depth limit, amount limit or cost analysis.
- Authorise in the business logic, on every node and edge. A check on the top-level query field does not protect objects reached through nested edges, the node field or a federated entity lookup.
- Trusted documents beat filtering for first-party APIs. If you control all clients, execute only reviewed, persisted operations and most query-shape attacks disappear.
- Hide the schema where you can, but do not rely on it. Disable introspection, schema exploration interfaces and field suggestions in production; field names can still be guessed, so the other controls must stand alone.
Why GraphQL needs its own controls
The GraphQL specification (latest release September 2025) defines a typed schema, query language and execution model, and leaves transport, authentication and resource limits to implementers. A GraphQL endpoint is usually one URL accepting arbitrary operations against the whole schema, which breaks three REST assumptions:
- One request is not one unit of work. The OWASP GraphQL Cheat Sheet notes that "HTTP request counts alone do not account for expensive queries or multiple operations in one request".
- The route does not identify the action. Gateways and web application firewalls see one GraphQL endpoint; the operation, fields and arguments live in the body.
- Data is reached through relationships. A user's order can be reached from the order query, the user's orders edge, a search result or a global node lookup, and each path is a separate resolver that can miss the check.
GraphQL threats and the controls that address them
| Threat | How it works (conceptually) | Primary control | Weakness class |
|---|---|---|---|
| Schema exposure | Introspection or a schema exploration interface returns every type, field and argument, including unused and internal ones | Disable introspection and exploration interfaces in production; remove dead fields | CWE-200 |
| GraphQL field suggestions | Validation errors suggest similar field names, letting the schema be inferred with introspection off | Disable suggestions where the server allows; mask validation detail outside development | CWE-209 |
| Deep or recursive queries | Cyclic relationships nest until the server exhausts CPU or memory | Depth limit, with a smaller limit on nested lists | CWE-770 |
| Huge result sets | Large page-size arguments or unpaginated lists return millions of nodes | Mandatory pagination and maximum page sizes | CWE-770 |
| GraphQL alias abuse | One field repeated under many aliases in one operation multiplies work or attempts | Breadth and alias limits; cost analysis counts each alias | CWE-770, CWE-307 |
| GraphQL batching abuse and multiplexing | Many operations in one request (array batching) bypass per-request rate limits and lockouts | Batch size limit; no batching for authentication, OTP and sensitive operations | CWE-307, CWE-770 |
| Broken resolver authorisation | Nested edges, node lookups or entity resolvers return objects the caller does not own | Object and field checks in the business logic layer for every path | CWE-639, CWE-862, CWE-863 |
| Injection through arguments | Argument values reach SQL, NoSQL, OS commands or URL fetches unsanitised | Typed inputs, custom scalars, parameterised data access | CWE-89, CWE-943, CWE-918 |
| Directive abuse | Client-supplied or repeated directives change execution or overload the parser | Allowlist directives per location, limit repeats, validate directive arguments | CWE-20, CWE-770 |
| Verbose errors | Stack traces and resolver errors reveal data sources and internals | Error masking in production, full detail only in server logs | CWE-209 |
| Cross-site request forgery | A browser sends a state-changing operation with ambient cookies as a simple request | No mutations over GET; require JSON content type or a custom header; token-based auth | CWE-352 |
GraphQL schema introspection and field suggestions
Introspection is part of the GraphQL type system: reserved names starting with two underscores expose the schema so tools can build clients. In production it gives anyone a complete map of operations, including fields the front end never calls. ASVS 5.0 requirement 4.3.2 (Level 2) is that "GraphQL introspection queries are disabled in the production environment" unless the API is meant for other parties.
- Private and first-party APIs: turn introspection off system-wide outside development. graphql.org says APIs serving only first-party clients "may forbid introspection queries in non-development environments".
- Public APIs: OWASP suggests disabling introspection for unauthenticated or unauthorised callers, or filtering it to approved roles, and publishing documentation instead.
- Exploration interfaces: OWASP says to "Disable GraphiQL and other similar schema exploration tools in production or publicly accessible environments". Interactive explorers bundled with servers are a common leftover.
- Field suggestions: "did you mean" hints in validation errors leak names. OWASP recommends disabling them once introspection is off; not every server supports this, in which case mask validation messages outside development. graphql.org warns an attacker "could ultimately infer the shape of an entire schema by running numerous operations with incorrect field names".
- Schema hygiene: remove unused and internal fields, and keep admin operations on a separate endpoint.
Automated schema exploration and field-guessing tools work even with introspection disabled, so treat schema hiding as reduced exposure, not a control.
GraphQL query depth, cost and complexity limits
graphql.org groups query depth limiting, pagination, batch limits and cost analysis as demand control. Apply them during validation, before any resolver runs, so a rejected query costs almost nothing.
| Control | What it bounds | How to set it | Limits |
|---|---|---|---|
| GraphQL depth limiting | Nesting levels; graphql.org recommends a separate, smaller limit for nested lists | Measure the deepest legitimate operation from your clients and add small headroom | Shallow but wide queries pass |
| Amount limiting and pagination | Objects returned per list field | Mandatory page-size arguments with server-enforced maximums; reject unbounded lists | Many paginated fields can still add up |
| Breadth and alias limits | Top-level fields and aliases per operation | Cap aliases per field and fields per selection set | Does not weigh expensive fields |
| Batch limits | Operations per HTTP request | One operation per request unless batching is needed; a low cap otherwise | Clients can send more requests |
| Query cost analysis | Estimated work, using weights per field and multipliers for list sizes | Weight fields by real resolver cost; reject over a per-request maximum and deduct from a per-client budget | Weights drift as resolvers change |
| Timeouts | Wall-clock time per operation and resolver | Application-level timeouts on operations and resolvers | Work already done is wasted, not prevented |
- GraphQL query cost analysis is the most complete GraphQL query complexity control: OWASP advises "enforcing a maximum allowed cost per query", and graphql.org describes rejecting requests that exceed a maximum cost or charging the cost against a client budget.
- Timeouts in the application, not just the proxy: OWASP warns that "infrastructure timeouts are often inaccurate and can be bypassed more easily than application-level ones".
- Back-end protection: request-scoped batching and caching of data loads (the data loader pattern) stop one query from issuing thousands of duplicate database calls, and container CPU and memory limits contain the rest.
GraphQL denial of service is listed by OWASP under API4:2023 Unrestricted Resource Consumption, whose example scenario is a single GraphQL request carrying many expensive upload operations to get past a per-request rate limit.
Batching, aliasing and GraphQL rate limiting
GraphQL batching sends several operations in one HTTP request, usually as an array; the GraphQL over HTTP specification does not define batching, so behaviour depends on the server. GraphQL aliasing lets one operation request the same field many times under different response names, which the specification allows so clients can fetch the same field with different arguments. Combined, they enable GraphQL multiplexing: hundreds of attempts in one request. OWASP lists the results: object enumeration, brute force of passwords, one-time codes and tokens, and denial of service that proxy rate limits never see.
- Rate limit in the business logic layer: graphql.org says "applying rate limits within the business logic layer is generally recommended". Count resolver executions or query cost per identity, not HTTP requests per IP.
- Protect authentication flows: login, OTP verification, password reset and token exchange should refuse batching and aliasing entirely, count every attempt against the account, and lock or step up on repeated failures. These map to ATT&CK T1110 Brute Force.
- Cap batches: OWASP recommends limiting how many operations can be batched and run at once, and calls batch limits "not a silver bullet", so pair them with cost limits.
- Per-operation throttles: API4:2023 asks for limits on how often a client can run a specific sensitive operation, such as OTP validation or recovery.
GraphQL authorisation and resolver design
OWASP lists broken authorisation, including IDOR, among the common GraphQL attacks. graphql.org states that authorisation "should happen in the business logic layer", with each GraphQL resolver delegating to it, so the GraphQL API, REST endpoints and background jobs share one source of truth. OWASP adds: "Always validate that the requester is authorized to view or mutate/modify the data they are requesting".
- Every edge and every node: OWASP says to "Enforce authorization checks on both edges and nodes". A check on the top-level orders field does not protect orders reached through a customer's orders edge or a search result.
- Global node lookups: the node and nodes fields fetch any object by global ID. Possession of an ID is not permission; authorise in the object loader so every path is covered.
- Field-level rules: restrict sensitive fields (personal data, internal notes, cost prices) by role or relationship, and use interfaces or unions to return different shapes to different callers.
- Mutation inputs: define explicit input types per mutation and bind only allowed fields, so a profile update cannot set a role or owner.
- GraphQL authorisation fan out: one query can touch thousands of objects, so a per-object call to an external policy service becomes thousands of calls. Batch authorisation decisions per request alongside data loads, and fail closed if the policy service is slow.
- Directives as declarations only: schema directives can mark which fields need which permission, but graphql.org says the logic itself should stay delegated to the business layer.
Pass resolvers a fully resolved caller (user, tenant, scopes and, for agents or services, the originating user) rather than a raw token. See the Authorisation Models Guide for choosing the model behind those checks.
GraphQL input validation and injection
GraphQL injection is ordinary injection arriving through arguments: the type system checks shape, not safety. The OWASP cheat sheet lists SQL, NoSQL, OS command, SSRF and CRLF injection and request smuggling among GraphQL attacks.
- Use the type system: enums for fixed choices, custom scalars for emails, URLs and identifiers with server-side validation, and input object types rather than free-form JSON scalars.
- Parameterise downstream: resolvers that build SQL, NoSQL filters or search queries from arguments must use parameterised APIs. A generic filter argument passed straight to a database query language is a common GraphQL-specific mistake.
- No user-chosen hosts: OWASP advises against making requests to user-supplied hosts unless there is an absolute business need; URL arguments go through the platform's request-forgery controls.
- GraphQL directive injection: directives such as skip and include change execution, and servers may expose custom ones. Reject unknown directives in operations, limit how many directives a field may carry, validate directive arguments like any other input, and never pass client values into schema or directive definitions built at runtime.
Persisted queries and trusted documents
A persisted GraphQL query is an operation stored on the server and called by ID. graphql.org calls the security form trusted documents: persisted documents "authored by your developers and approved through code review", where "the server will only execute requests for known document IDs".
| API type | Recommended model | Why |
|---|---|---|
| First-party web and mobile clients only | Trusted documents, arbitrary operations rejected in production | Removes depth, alias, batching and introspection abuse at once; limits are only needed on reviewed operations |
| First-party plus a few partners | Trusted documents for your clients; a separate partner endpoint with full demand control | Keeps the strict path strict |
| Public API for third-party developers | Open operations with cost analysis, depth, amount and batch limits, per-client budgets | graphql.org notes trusted documents "can't be used for public APIs" |
Hash-registration schemes that let any client register a new document on first use are a caching optimisation, not an allowlist; only documents registered from your build pipeline count as trusted. Release the client and its documents together, and keep the previous document set live until old app versions age out, which is also the rollback path.
Federated GraphQL
GraphQL federation composes one schema from independent subgraph services behind a gateway; the GraphQL Foundation's Composite Schema Working Group is developing an official specification. Federation adds its own exposure:
- Subgraphs must not be reachable directly. Restrict them to the gateway with network policy and mutual TLS or signed gateway requests; a public subgraph has no demand control.
- Entity resolution is an entry point. The mechanism that lets the gateway fetch an entity by key from a subgraph bypasses top-level query fields, so authorise in the entity loader as well.
- Limits apply to the whole plan. Enforce depth and cost at the gateway on the client operation, and timeouts per subgraph call.
- One identity model: propagate the verified caller and tenant to every subgraph, and keep authorisation in each subgraph's business logic, not only at the gateway.
Transport, CSRF and error handling
- No mutations over GET: the GraphQL over HTTP draft says "GET requests MUST NOT be used for executing mutation operations".
- CSRF: the same draft notes that form-encoded and multipart bodies can be sent as simple requests without a CORS preflight. Accept only JSON request bodies for operations (or require a custom header), and prefer bearer tokens to cookies.
- Errors: OWASP: "GraphQL APIs in production shouldn't return stack traces or be in debug mode". Log full detail server-side with the operation name and a correlation ID.
- Client credentials: see the API Key Management Guide for keys and tokens held by GraphQL clients.
Testing, detection and metrics
- Testing: confirm introspection, exploration interfaces and suggestions are off in production; send over-limit depth, alias, batch and cost operations in staging and confirm rejection before execution; run two-user authorisation tests through every path to each sensitive type (query, edges, node, entity resolution); fuzz arguments for injection. See the Penetration Testing Guide.
- Log per operation: operation name or trusted document ID, caller and tenant, computed cost, depth, alias and batch counts, resolver error counts, rejection reason and duration.
- Detect: introspection attempts in production, bursts of validation errors with unknown field names (schema guessing), operations near cost or alias limits, one identity resolving many distinct object IDs, and authentication mutations at a rate above one attempt per request.
Metrics that show the controls are working:
- Share of production traffic served from trusted documents (first-party APIs, target near 100%).
- Operations rejected by depth, cost, alias or batch limits per day, and false positives raised by client teams.
- 95th percentile query cost and resolver execution time per client.
- Percentage of types with object-level authorisation tests through every access path.
- Production endpoints, including subgraphs, answering introspection (target zero).
Implementation roadmap
- Inventory: find every GraphQL endpoint and subgraph, including staging hosts reachable from the internet, and record which allow introspection.
- Close the easy exposure: disable introspection, exploration interfaces, suggestions and stack traces in production.
- Measure before limiting: log depth, aliases, batch size and estimated cost for all production operations for two to four weeks, in report-only mode.
- Enforce limits set from those measurements, starting with batch and alias caps on authentication operations, then depth, amount and cost. Keep limits in configuration so they can be raised without a redeploy if a legitimate client breaks.
- Fix authorisation paths: move checks into the business logic layer and add two-user tests for every path to sensitive types.
- Adopt trusted documents for first-party clients, first in log-only mode (flag unknown documents), then reject.
- Lock down federation: make subgraphs gateway-only and add authorisation to entity loaders.
Framework mapping
| Framework | Where GraphQL security sits |
|---|---|
| OWASP ASVS 5.0 | 4.3.1 query allowlist, depth, amount or cost limits; 4.3.2 introspection disabled in production; V8 Authorization for resolver checks |
| OWASP API Security Top 10 2023 | API1 BOLA and API5 BFLA (resolver authorisation); API3 property level (field rules); API4 Unrestricted Resource Consumption (depth, cost, batching) |
| CWE | CWE-770 and CWE-400 (resource limits); CWE-307 (batched brute force); CWE-639, 862, 863 (authorisation); CWE-200, CWE-209 (schema and error exposure); CWE-89, 943 (injection) |
| MITRE ATT&CK | T1190 Exploit Public-Facing Application; T1110 Brute Force for batched credential and OTP guessing |
| NIST SP 800-53 Rev. 5 | AC-3 Access Enforcement; SC-5 Denial-of-Service Protection; SI-10 Information Input Validation; SI-11 Error Handling |
| ISO/IEC 27001:2022 Annex A | A.8.26 Application security requirements; A.8.28 Secure coding; A.8.29 Security testing in development and acceptance |
Common failure patterns
- Introspection off, everything else open: teams treat a hidden schema as the security control while depth, batching and resolver authorisation stay unlimited.
- Rate limiting by HTTP request: a login mutation repeated under many aliases in one request gets one count. OWASP warns that proxy limits miss batching.
- Top-level authorisation only: the query field checks ownership, the nested edge and node lookup do not. This is the GraphQL form of IDOR.
- Staging endpoints on the internet: development servers with introspection and exploration interfaces enabled, pointed at production data.
- Directly reachable subgraphs: the gateway enforces limits and authentication, but subgraphs answer anyone who finds them.
- Generic filter arguments: a JSON filter scalar passed straight to the database turns a GraphQL API into a query language proxy.
Practitioner checklist
- Disable introspection, schema exploration interfaces, field suggestions and stack traces on every production endpoint and subgraph.
- Enforce depth (with a lower nested-list limit), amount, alias, batch and cost limits before execution.
- Apply application-level timeouts per operation and per resolver.
- Rate limit by resolver executions or cost per identity, and refuse batching and aliasing on authentication, OTP and recovery operations.
- Authorise in the business logic layer for every node, edge, node lookup, entity resolver and sensitive field.
- Use typed inputs and parameterised data access; reject unknown directives and generic filter scalars.
- Use trusted documents for first-party clients and reject arbitrary operations in production.
- Keep subgraphs reachable only from the gateway and propagate a verified caller to each.
- Reject mutations over GET and form-encoded operation bodies.
- Log operation name, cost, depth, batch size and caller, and alert on schema guessing and limit pressure.
Standards and references
- GraphQL Foundation: GraphQL Specification, September 2025 Edition
- GraphQL Foundation: GraphQL over HTTP Specification
- GraphQL Foundation: Security
- GraphQL Foundation: Authorization
- OWASP: GraphQL Cheat Sheet
- OWASP: Application Security Verification Standard 5.0, V4 API and Web Service
- OWASP: API4:2023 Unrestricted Resource Consumption
- MITRE: CWE-770 Allocation of Resources Without Limits or Throttling
Related NHI Mgmt Group resources: Secure Coding Guide · Authorisation Models Guide · API Key Management Guide · Penetration Testing Guide