GraphQL Security
GraphQL's flexibility is also its main security challenge. A single endpoint accepts arbitrary queries: clients can nest relationships ten levels deep, request thousands of items, alias the same expensive field a hundred times, or send dozens of operations in one HTTP request. Authorization can't be handled per URL like in REST, because one query may touch dozens of types. Data exposure bugs and denial-of-service risks look different in GraphQL, and they need different defenses.
The fundamentals of API security still apply: authenticate, authorize every access, validate input, and rate-limit. GraphQL adds query cost analysis, persisted queries, and careful schema exposure on top.
TL;DR
- Authenticate at the transport layer (tokens, sessions) and pass the viewer into the resolver context.
- Authorize in the business layer, per object and per field. Never rely on "the client won't ask for it".
- Limit query depth, complexity/cost, page sizes, aliases, and batched operations.
- Rate-limit by query cost, not just requests per minute.
- In production, prefer trusted persisted queries (an allowlist) for first-party clients, and disable or restrict introspection for private APIs.
- Return safe error messages, and never leak stack traces or internal details.
Quick Example
Apollo Server hardening with depth and cost limits plus resolver-level authorization:
Core Concepts
Authentication vs Authorization
- Authentication happens once per request, before GraphQL execution: validate a JWT, session cookie, or API key, and put the resulting viewer in the context.
- Authorization must happen wherever data is accessed. Because any query can reach any type through relationships (
order → customer → paymentMethods), check permissions at the object and field level, ideally in the business or data layer that resolvers call, so every path is covered.
Broken object-level authorization (BOLA/IDOR), where a user fetches order(id: "someone-else's"), is the top API vulnerability in OWASP's API list, and GraphQL's node(id:) fields make it especially easy to probe. See OWASP Top 10.
Resource Exhaustion Attacks
Query Cost Analysis
Assign costs to fields (for example, list fields multiplied by first, expensive resolvers weighted higher), compute a query's total cost during validation, and reject queries over a budget. Cost also enables cost-based rate limiting: each client gets a budget of cost points per minute. Public GraphQL APIs such as GitHub and Shopify publish their cost models so clients can plan.
Persisted Queries
- Automatic Persisted Queries (APQ) send a query hash instead of the full text to save bandwidth. This is a performance feature, not security, because any query can still be registered.
- Trusted documents / persisted query allowlists: first-party clients register their operations at build time, and the server executes only known operations by ID. Arbitrary queries from attackers are rejected outright, which eliminates most query-shape attacks for private APIs.
Introspection and Schema Exposure
Introspection lets anyone download the full schema. That's great for development tooling, and a map for attackers on private APIs. For internal or first-party-only APIs, disable introspection in production (or restrict it to authenticated staff), and turn off field suggestions ("Did you mean adminNotes?") that leak field names. Remember this is defense in depth: hiding the schema doesn't replace authorization.
Other Considerations
- Input validation: validate arguments beyond their GraphQL types (lengths, formats, ranges) with custom scalars or validation layers. Prevent injection by using parameterized queries in resolvers.
- CSRF: GraphQL over
GETor with simple content types can be vulnerable to CSRF when using cookie auth. Requireapplication/jsonPOSTs or a CSRF token (Apollo Server's CSRF prevention does this). See CSRF. - Subscriptions: authenticate WebSocket connections at connection init, and re-check authorization for each event delivered. See GraphQL subscriptions.
- Federation: subgraphs should only accept traffic from the router, and authorization must hold consistently across services. See GraphQL federation.
- Logging: log operation names, costs, and viewer IDs, but redact variables that may contain secrets or PII.
Best Practices
Centralize Authorization Logic
Put permission checks in a shared business layer or policy module (or schema directives like @auth backed by it), not scattered ad hoc through resolvers. Every resolver path to an object then enforces the same rules.
Use Allowlists for First-Party Clients
If only your own web and mobile apps call the API, trusted persisted queries remove the attack surface of arbitrary queries almost entirely, and improve caching and performance as a bonus.
Set Timeouts and Limits at Every Layer
Beyond validation rules: request body size limits, execution timeouts, database statement timeouts, and per-resolver timeouts for downstream calls.
Test Security Like Functionality
Write tests asserting that users can't access others' objects via every path (direct queries, node(id:), nested relationships), and that oversized or deeply nested queries are rejected. Tools like InQL, graphql-cop, and Escape help audit GraphQL endpoints.
Common Mistakes
Authorizing Only at the Top-Level Query
Authorize every type and sensitive field, not just entry points.
Relying on Hidden Introspection
Disabling introspection without authorization checks is security through obscurity: attackers can guess field names or extract them from client bundles. Treat it as a small extra layer, never the main one.
Rate Limiting Only HTTP Requests
One HTTP request can contain a batched array of 1,000 operations or one query with 1,000 aliases. Rate-limit and cost-limit per operation and per query cost.
FAQ
Is GraphQL less secure than REST?
Not inherently, but it shifts where risks sit. REST can lean on per-endpoint authorization and naturally bounded responses. GraphQL needs object- and field-level authorization, query cost controls, and deliberate schema exposure. Teams that apply REST habits unchanged often miss these.
Should I disable introspection in production?
For private APIs used only by your own clients, usually yes, or restrict it to authenticated internal users. For public APIs intended for third-party developers, keep it on; the schema is documentation. Either way, authorization must not depend on it.
How do I choose depth and complexity limits?
Measure your real client operations. Set depth slightly above your deepest legitimate query (often 7–10), and complexity at a comfortable multiple of your most expensive legitimate query. Log rejected queries to tune limits, and give clients clear error messages.
What are trusted documents?
A persisted-query allowlist: client operations are extracted at build time and registered with the server, which then executes only those operations by ID. It's the strongest defense against malicious query shapes for first-party APIs.
Related Topics
- GraphQL — The query language overview
- API Security — Security principles for all APIs
- GraphQL Schema Design — Designing a schema that's safe to expose
- GraphQL Pagination — Bounded lists
- Rate Limiting — Limiting request volume and cost
- OWASP Top 10 — Common web and API vulnerabilities