GraphQL Hacking: The Complete Practical Guide
GraphQL Authorization Flaws
How authorization breaks in GraphQL: authz lives on the resolver not the query, so an unguarded nested edge reaches protected objects you should not see.
Published
GraphQL authorization breaks when the guard is attached to a query path instead of to the resolver. Because the graph offers many routes to the same object, a type guarded on its direct query is often reachable through an unguarded nested edge. This is broken access control, BOLA and BFLA, inside the graph. Practice on a free live target.
Authorization belongs on the resolver
The shape of a query never proves intent. If authorization is checked only on the top-level user(id:) field, every other path that returns a User (as a nested field of an order, a team, a comment) must repeat the check, because each is served by its own resolver. When one resolver forgets, the object is exposed through that edge regardless of the guard elsewhere.
Reaching a protected object through an unguarded edge
Suppose user(id:) enforces that you can only read yourself. The direct path is blocked:
POST /graphql HTTP/1.1
Host: target.example
Content-Type: application/json
{"query":"{ user(id: 2) { email } }"}HTTP/1.1 200 OK
Content-Type: application/json
{"errors":[{"message":"Not authorized to view this user"}]}But the same User is reachable as order.customer, and that resolver never checks ownership:
POST /graphql HTTP/1.1
Host: target.example
Content-Type: application/json
{"query":"{ order(id: 1007) { id customer { id email } } }"}HTTP/1.1 200 OK
Content-Type: application/json
{"data":{"order":{"id":"1007","customer":{"id":"2","email":"[email protected]"}}}}You read another user's email by navigating to it through an order. The direct guard was real, the nested edge was not. That is BOLA in the graph.
Mutation gating that forgets one path
The same gap appears on writes. An admin-only setUserRole that checks the caller's role is BFLA if a different mutation (say updateTeam) can change roles as a side effect without the check. Enumerate every mutation that touches a sensitive field, not just the obviously privileged one.
Confirm without destroying data
Prove the bypass with a read, not a destructive write:
read a victim object order.customer.email, message.author.privateNote
read a protected field User.passwordResetToken via a nested edge
dry-run a mutation call it with a harmless value and check the authz response, not the effectReading someone else's data demonstrates the flaw cleanly. Avoid running a destructive mutation on a shared target.
Edge variants
node(id: "...") a Relay global-id node resolver can fetch any type by opaque id
aliased nested fields request several victims' objects in one request via aliases
list vs detail a list field may be scoped while the detail edge is not (and vice versa)Fixing it
- Authorize in every resolver against the authenticated principal, on every path to an object.
- Do not trust the query shape. A guarded top-level field does not protect the type on nested edges.
- Centralize object-level checks (for example a shared "can this user see this object" function) so no edge is missed.
Full guidance is in preventing GraphQL attacks. For the full class, see broken access control. Next, close the whole surface in preventing GraphQL attacks, or try an authz bypass on a free lab.