Design Partner Program
GUIDE · GRAPHQL SECURITY

GraphQL security testing: one endpoint, every resolver, every caller.

GraphQL lets the client decide what to fetch, how deep to go and how many operations to send at once. That flexibility moves security from the route to the resolver, and it answers almost everything with 200 OK. This guide covers what to test, how to tell a secure response from a leak, and how to keep it tested.

ANATOMY OF ONE QUERYPOST /graphql
query { OperationQuery, mutation, subscription
project(id: "prj_B") { ArgumentAnother org's ID
sharedWith { members { Nested pathRelationships that skip checks
email billing { … } FieldsSensitive fields on allowed types
a2: project(…) a3: … AliasesMany operations, one request
← 200 { data, errors } ResponseAlways 200; read the body
Every nested field is a resolver, and every resolver needs its own authorization check.
01 · TEST CASES BY ATTACK CLASS

GraphQL Security Test Cases, by Attack Class

GraphQL has no per-endpoint verbs to iterate over, so organize tests by what the attacker is trying to do. Pick a class to see the operation to send and the response a secure API returns.

Checks belong on every type and field, not just the root query. API1 · API3 · API5 (2023)
TEST SEND SECURE RESPONSE

Object by ID

Fetch another org's object directly.
query { project(id: "prj_B") { name } } as alice null + FORBIDDEN error; no fields returned.

Nested traversal

Reach forbidden data through a relationship.
{ project(id:"prj_A") { sharedWith { members { email } } } } null on sharedWith for orgs alice isn't in.

Field-level access

Ask for a sensitive field on an allowed object.
{ me { org { billing { cardLast4 } } } } as member null + FORBIDDEN on billing for non-admins.

Node interface

Global node lookups often skip type checks.
{ node(id: "VXNlcjpCMQ==") { ... on User { email } } } null Same rules as the typed query.
02 · TEN OPERATIONS, ALL 200 OK

GraphQL Security Test Run: Why the Status Code Tells You Nothing

Ten operations against one schema, as three identities. Every response is 200. The verdict comes from what's inside: the data returned, the errors raised and how much work the server did.

projects-graph · staging identities: alice (org A) · bob (org B) · anon
Run0/10Pass0Fail0
# OPERATION AS HTTP EXPECTED ACTUAL VERDICT
01 project(id: prj_A) { name }Member reads own project alice ·· data.project ·· Pending
02 project(id: prj_A) { name }No token anon ·· UNAUTHENTICATED ·· Pending
03 project(id: prj_A) { name }Other org by ID bob ·· null + FORBIDDEN ·· Pending
04 project { sharedWith { members { email } } }Nested traversal alice ·· null + FORBIDDEN ·· Pending
05 me { org { billing { cardLast4 } } }Sensitive field as member alice ·· null + FORBIDDEN ·· Pending
06 __schema { types { name } }Introspection anon ·· error ·· Pending
07 me { passwrd }Field suggestions alice ·· generic error ·· Pending
08 user { friends ×7 }Depth 7 query alice ·· depth limit error ·· Pending
09 verifyOtp ×500 aliasesAlias brute force anon ·· rate limited ·· Pending
10 updateMe(input: { role: ADMIN })Privileged input field alice ·· error ·· Pending
Press play to run ten operations. Watch the HTTP column: it never changes.
00:00 / 00:20

Illustrative run · synthetic schema and data · 20 s replay

03 · WHAT A FINDING LOOKS LIKE

Nested Authorization Example: Walking the Graph Into Another Org

The root project query checks membership. The nested sharedWith and members resolvers don't. Three hops later, alice is reading another organization's people.

Testing REST APIs instead? →

Illustrative finding · synthetic data

Projects graph · cross-org data via nested resolvers API1:2023 · API3:2023 · CWE-863
Critical Validated
SETUP
Identity: alice, member of org A. Project prj_A is shared with org B for a joint workstream.
BASELINE
alice reads her own project. Root resolver checks org membership.{ project(id:"prj_A") { name } } → data.project.name
TRAVERSE
She follows the share relationship into org B.{ project(id:"prj_A") { sharedWith { name members { email role } } } }
RESPONSE
Org B's member list comes back in full. Emails redacted in the evidence record.200 sharedWith: [{ name:"Org B", members:[ 12 × { email:"█████", role } ] }]
REACH
The same path works from any project shared with any org: one shared project exposes the whole partner directory.
SCANNER
A REST-oriented scanner saw one endpoint returning 200 and no error signature.
Fix and retest: enforce authorization on the Organization and User types, not only at the root query; the same operation is replayed and sharedWith now resolves to null with a FORBIDDEN error.
04 · SECURE RESPONSES

What a Secure GraphQL Response Looks Like

In GraphQL, assertions target the body. Write each test against one of these shapes, and treat anything else as a finding.

Forbidden field

{ data: { project: null }, errors: [{ extensions:{ code:"FORBIDDEN" } }] }

Caller is authenticated but may not read this object or field.

RED FLAG · Data returned, or a partial object with some fields filled.

Not authenticated

errors: [{ extensions:{ code:"UNAUTHENTICATED" } }]

No token, or an invalid or expired one.

RED FLAG · Anonymous callers receive any non-public data.

Validation error

errors: [{ message:"Cannot query field" }]

Unknown field, bad argument or input type mismatch.

RED FLAG · "Did you mean …" suggestions in production.

Depth or cost rejected

errors: [{ message:"Query too complex" }]

Query exceeds the depth, cost or alias limits.

RED FLAG · Slow success: the server ran the whole thing.

Rate limited

errors: [{ extensions:{ code:"RATE_LIMITED" } }]

Caller exceeds limits counted per operation or per field.

RED FLAG · Hundreds of aliased attempts all executed.

Persisted query only

errors: [{ message:"PersistedQueryNotFound" }]

Server accepts only pre-registered operations from first-party clients.

RED FLAG · Arbitrary queries accepted from the public client.

Introspection off

errors: [{ message:"introspection disabled" }]

__schema and __type requested in production.

RED FLAG · Full schema returned to anonymous callers.

Generic server error

errors: [{ message:"Internal error" }]

Something failed; details logged server-side only.

RED FLAG · Stack traces, SQL or resolver paths in extensions.

Error codes vary by server; FORBIDDEN and UNAUTHENTICATED follow common conventions. Use whatever your server documents, consistently.

05 · THE WORKFLOW

How to Test a GraphQL API: From Schema to CI

Six steps that cover every type and field, not just the operations your client happens to send.

Choosing an API security testing tool →
01

Get the full schema

Use the SDL from the repo or introspection in staging. Compare it with production to catch debug types and mutations.

02

Map types to owners

For each type, record who may read it and which fields need extra privileges. Most schemas don't encode this.

03

Generate paths, not just operations

Build queries that reach each sensitive type through every relationship, since each path is a separate check.

04

Run as every identity

Two users per role across two orgs, plus anonymous. Compare what each one receives for the same operation.

05

Assert on body, cost and limits

Expect specific nulls and error codes; time and count work for depth, alias and batching tests.

06

Run on schema change

Trigger the suite whenever the schema or a resolver changes; new fields and relationships are where leaks appear.

06 · WHERE HAYROK FITS

GraphQL Validation That Checks Every Resolver

Hayrok reads your schema, builds operations that reach every type through every path, and runs them as each identity you define. It asserts on data and errors rather than status codes, and records each leak with the operation that caused it.

01

Schema-aware coverage

Builds operations from your SDL that reach every type through every relationship.

02

Every identity, every path

Runs each operation as each role and org, and compares what comes back.

03

Body-level assertions

Judges data, nulls and error codes, not HTTP status.

04

Cost and batching scenarios

Depth, alias and batch tests run within approved limits on staging.

05

Evidence per leak

Each finding stores the operation, the identity and the redacted response in Evidence Fabric.

07 · FAQ

GraphQL Security Testing FAQ

Testing a GraphQL API the way an attacker would: walking relationships into data you shouldn't see, requesting sensitive fields, abusing introspection, sending expensive or batched queries and setting privileged mutation inputs, then checking the data and errors returned.

BRING YOUR SCHEMA

Find the resolvers that skip authorization before someone walks the graph.

Share an SDL file or staging endpoint and three test identities. We'll test every type through every path and show you each leak with the operation that caused it.