APIs & Auth
GraphQL Hacking: The Complete Practical Guide
How to hack GraphQL APIs: find the endpoint, dump the schema via introspection, inject through arguments, abuse batching, and prevent it. With free practice.
Published

GraphQL is a query language where the client, not the server, dictates the exact shape of the response. That power becomes a problem when the server over-exposes its schema, enforces authorization unevenly across the graph, or passes arguments straight into a database. It also breaks the habits REST testers lean on: one URL, every operation in a single POST body, no routes to walk, but a self-describing type system you can interrogate for free. The headline risks are introspection leakage, per-resolver authorization gaps, injection through arguments, and batching or alias abuse that sidesteps rate limits. Want to feel it rather than read about it? Sign up free and try it on a live target.
What is GraphQL?
The mental model in one sentence: one endpoint, and the client asks for exactly the fields it wants. Instead of many REST routes, a GraphQL API exposes a single endpoint (usually /graphql) that accepts a query describing the precise shape of the response. There are three operation kinds: queries (read), mutations (write), and subscriptions (real-time streams). The server publishes a typed schema of every type, field, and argument, and each field is backed by a resolver function that fetches its data.
The self-describing type system is the gift to an attacker. The first spoke covers the model in full: what is GraphQL.
Finding the endpoint
GraphQL hides behind a handful of conventional paths, and a trivial query confirms it. The detection move and the tells (GraphiQL, Playground, Apollo Sandbox, the SPA bundle) are in finding GraphQL endpoints.
Introspection: the free schema dump
When introspection is left on, one query returns the entire schema. Reading the dump, mapping the mutations, and spotting the dangerous ones (anything touching users, roles, or payments) is covered in introspection and schema discovery.
When introspection is disabled
Turning off __schema is not enough. Many servers still leak field names through suggestions ("Did you mean ...?"), letting you rebuild the schema by fuzzing. See bypassing introspection defenses.
Injection through arguments
GraphQL validates the type of an argument, never its value. A String argument happily carries a SQL payload into a resolver that concatenates it into a query. The injection lives in the resolver, not in GraphQL. See unsanitized argument injection.
Batching and alias abuse
Because one request can carry many operations (via aliases or a batch array), per-request rate limits do nothing. That turns a login or OTP endpoint into a brute-force target under a single HTTP call. See batching and alias abuse.
GraphQL CSRF
A normal application/json GraphQL POST is not cross-site forgeable, because it triggers a CORS preflight. But an endpoint that also accepts a form-encoded or text/plain body (or GET for a state-changing operation) reopens CSRF. See GraphQL CSRF.
Authorization flaws
Authorization belongs on the resolver, against the authenticated user, not on the shape of the query. When one path through the graph is guarded and a nested edge to the same object is not, you reach data you should not. This is BOLA and BFLA inside the graph. See authorization flaws.
A worked example
Say you find a single endpoint and want to know what it exposes. First confirm it is GraphQL with the cheapest possible query:
POST /graphql HTTP/1.1
Host: target.example
Content-Type: application/json
{"query":"{ __typename }"}HTTP/1.1 200 OK
Content-Type: application/json
{"data":{"__typename":"Query"}}A GraphQL response body (not a 404) confirms the endpoint, regardless of whether the root type uses the conventional name Query. Now ask for the mutation list through introspection:
POST /graphql HTTP/1.1
Host: target.example
Content-Type: application/json
{"query":"{ __schema { mutationType { fields { name } } } }"}HTTP/1.1 200 OK
Content-Type: application/json
{"data":{"__schema":{"mutationType":{"fields":[{"name":"updateUser"},{"name":"setUserRole"},{"name":"deletePost"}]}}}}setUserRole stands out: a mutation that changes a user's role is exactly the kind of over-exposed, under-authorized operation the UI never surfaces. From here you would introspect its arguments, then test whether the resolver checks that the caller is allowed to call it. That is the arc: find the endpoint, confirm it, dump the schema, and pivot to the operation that should never have been reachable.
Prevention
- Disable introspection and field suggestions in production. Both hand attackers the schema.
- Enforce depth and cost limits. Cap nested and circular queries so a single request cannot exhaust the server.
- Authorize inside every resolver against the authenticated principal. The query shape never proves intent.
- Parameterize all resolver database access. Never build queries by string concatenation.
- Require
application/json, reject GET for mutations, and apply CSRF protection. Prefer persisted or allow-listed queries. - Rate-limit by operation and resolved work, not just by HTTP request, and cap aliases and batch size.
The full detail is in preventing GraphQL attacks.
Where it connects
GraphQL is a transport that can carry other bugs. Its argument-injection surface is SQL injection and NoSQL injection reached through a resolver. Its authorization-flaw surface is broken access control, BOLA and BFLA expressed inside a graph. And its forgeable-mutation surface is CSRF when a lax content-type reopens the cross-site request.
Where to go next
Work through the cluster in order, or jump to what you need. Start with what is GraphQL, then finding the endpoint and introspection. When you want every query in one place, the GraphQL cheat sheet has them. And when you are ready to break something real, sign up free and start hacking.