Test the resolver tree
Begin with one allowed top-level query. Add a nested field that points at a foreign account, then add an alias and a fragment. The returned data must follow the same access rule in every query shape. Repeat for mutations that accept object input. When the server reports a field error, inspect any partial data returned with it. Check subscriptions if the schema exposes them; the permission can change while a connection stays open.
Test cases and proof
| Case | Expected result | Proof to keep |
|---|---|---|
| Query forbidden field by alias | Field stays denied | Response shape |
| Traverse from allowed parent to foreign child | Deny child | Nested response |
| Mutation changes hidden property | Reject write | Stored record |
Synthetic example
A customer query can include an allowed account and a nested transactions field. Use only arguments the nested field supports, then repeat the reachable path with an alias and fragment. A resolver must check each returned object and sensitive field; a top-level query check is not enough.
Evidence to keep
Save the exact GraphQL document and variables for each case. The same field may appear under a different alias, so preserve both the submitted name and the response key. Record the user role, target object owner, returned data, and errors array. If a mutation reports an error after making a change, treat that as a failed authorization check and inspect the stored record.
Also try a mutation that changes an allowed field and a forbidden field in the same input. The server should either reject the whole request or apply only the allowed field under a documented rule. Read the row after the call. This catches a write that a GraphQL error response can hide.
Keep security at each resolver
A top-level account query may check ownership while a nested transaction resolver loads by ID alone. That creates a leak even though the request uses one /graphql route. Map which resolver reads each object and where it gets the authenticated user. A denial may return null for one field with an error, which is fine only if the rest of the data belongs to the caller. Run the same nested case through every query form the schema accepts. Query complexity limits protect capacity, not ownership.
Related reading
Why these checks matter
The OWASP GraphQL guide covers field access and query cost. The authorization guide calls for permission checks on every request. Together they point to two distinct tests: whether a nested resolver returns a foreign record and whether one query can consume too much server work. A depth limit does not replace a record access check.
Use paths the schema actually exposes
Define the fixture before writing the query. A owns account A1 and transfer TA; B owns account B1 and transfer TB. If the schema has account(id), transfer(id), and node(id), test each direct lookup as A with B’s ID. If account.transfers has no ID argument, asking it for a B transfer by changing a nonexistent argument is not a valid test. Instead check whether its returned list contains TB, or use a real search edge that can reach TB.
Use an alias and fragment on the same actual field paths. Save the complete document and variables. Expected data is limited to A-owned objects; errors may coexist with permitted data. Read response paths and stored rows rather than treating an HTTP 200 or an errors array as the whole verdict.
Keep cache and mutation scope explicit
DataLoader often batches reads. Create it per request or partition its cached results by trusted caller scope. Run A’s B1 lookup and B’s allowed B1 lookup on separate requests, then reverse their order. A must never inherit B’s cached authorization result. A process-wide loader keyed only by object ID can break that rule.
For mutations, test one operation with two top-level fields: an allowed update followed by a forbidden update. GraphQL does not make the whole operation a database transaction. The first permitted change may commit even if the second field fails. Separately test a single mutation input containing one allowed and one forbidden property under its documented atomicity rule. GraphQL’s authorization guide places policy in the trusted business layer; inspect every resolver path that reaches it.