GraphQL Hacking: The Complete Practical Guide
Preventing GraphQL Attacks
How to secure a GraphQL API: disable introspection and suggestions, limit query depth and cost, authorize every resolver, parameterize queries, and block CSRF.
Published
GraphQL has one real security model and several supporting controls. The real model is that every resolver authorizes and parameterizes its own work. Everything else, disabling introspection, limiting cost, blocking CSRF, reduces surface or blast radius. This page is the defensive reference for the cluster. Want to see why the weak controls fail? Practice the bypasses on a live target.
Disable introspection and field suggestions
Both hand an attacker the schema. Disable introspection in production, and disable field suggestions too, because suggestions rebuild the schema by fuzzing even with introspection off:
introspection: off in production
field suggestions: off (on JS stacks, graphql-armor block-field-suggestions)
GraphiQL / Playground / Sandbox: not exposed on live endpointsThis reduces reconnaissance. It is not a security boundary, so never let it stand in for the controls below.
Limit query depth and cost
Stop nested, circular, and aliased queries from exhausting the server:
- Depth limit: cap how many levels a query can nest, which kills circular blow-ups.
- Cost / complexity analysis: weight each field (and list sizes) and reject queries over a budget. More precise than depth alone, since it catches shallow-but-huge queries.
- Alias and batch caps: bound how many times one operation can repeat a field, and disable array batching if unused.
Authorize in every resolver
Authorization belongs on each resolver, evaluated against the authenticated principal, on every path to an object:
- Check object-level access (can this user see this object) on nested edges, not just top-level fields.
- Check function-level access (can this role call this operation) on every state-changing mutation.
- Centralize the checks so no path through the graph is left unguarded.
The query shape never proves intent. A guarded top-level field does not protect the type when it is reachable as a nested field elsewhere.
Parameterize everything in resolvers
GraphQL validates argument types, not values, so the resolver must treat every argument as hostile:
- Use bound parameters or an ODM that escapes operators. Never build a query by string concatenation.
- Validate values, not just types: allow-list enum-like inputs and reject unexpected keys in input objects so a database operator cannot ride in.
Block CSRF and lock the transport
- Require
application/jsonand reject form-encoded,text/plain, and GET for operations, which restores the CORS preflight that protects you. - Reject mutations over GET, per the spec.
- Add CSRF protection (a token or a required custom header) and set
SameSitecookies.
Rate-limit by work, not by request
A per-request counter is defeated by one aliased request. Rate-limit by operation and resolved work, counting fields and cost, so hundreds of aliased attempts in one request are counted as hundreds.
Persisted and allow-listed queries
The strongest lockdown: register the exact queries the client is allowed to send (persisted queries) and reject anything else. The server then runs only known operations, which neutralizes arbitrary introspection, injection probing, and ad-hoc aliasing in one move.
The layered picture
- Primary: every resolver authorizes against the user and parameterizes its data access.
- Supporting: introspection and suggestions off, depth and cost limits, alias and batch caps, strict content type and CSRF protection, work-based rate limiting, persisted queries.
With the primary control in place, the rest is defense in depth. Without it, the rest only slows an attacker down.
For the attack side of each of these, work back through the cluster from the pillar, and keep the cheat sheet handy. Ready to prove the weak controls fall? Sign up free and try it.