IDOR in GraphQL
GraphQL doesn’t change what IDOR is - it’s still a missing object-level authorization check - but it changes how you find and exploit it. Objects are fetched by argument (user(id: 42)) through a single endpoint, the schema is often self-describing via introspection, and aliases/batching let you pull hundreds of objects in one request. If the resolver authorizes the query but not the object, every one of those is an IDOR.
# 1. Map the schema (if introspection is on)query { __schema { queryType { fields { name } } types { name fields { name } } } }
# 2. Direct object access - swap the idquery { user(id: 42) { id email role } }
# 3. Mass-enumerate in ONE request via aliases (bypasses per-request rate limits)query { u1: user(id:1){email} u2: user(id:2){email} u3: user(id:3){email} }
# 4. Relay global node IDs are usually base64("Type:pk")# "VXNlcjo0Mg==" -> "User:42"; decode, change the pk, re-encodequery { node(id: "VXNlcjo0Mw==") { ... on User { email } } }Success indicator: a field you shouldn’t see (another user’s email, a private record) comes back - or a mutation on someone else’s object succeeds.
Why GraphQL is IDOR-prone
Section titled “Why GraphQL is IDOR-prone”- One endpoint, many objects.
/graphqlresolves any object the schema exposes; developers often put authentication on the endpoint and forget per-object authorization in the resolvers. - Introspection hands you the map. When enabled (common outside production, and left on in production more than it should be),
__schemalists every type, field, query, and mutation - no guessing which objects exist. - Aliases and batching remove the rate limit. A single document can request the same field hundreds of times under different aliases, so the per-request throttles that would slow REST mass enumeration don’t apply.
- Mutations are object calls too.
updateUser(id: 42, role: ADMIN)is the GraphQL form of the insecure-API function-call IDOR.
Step 1 - Map the schema
Section titled “Step 1 - Map the schema”- Try introspection. If it’s on, pull the full type/field/query/mutation list; a tool like GraphQL Voyager or InQL (Burp) renders it.
- If introspection is disabled, fall back to field suggestions (“did you mean …” errors leak names), known-schema wordlists, or the client-side JS, which often contains the queries.
- Note every query/mutation that takes an
id/uuid/object-reference argument - those are your IDOR candidates.
Step 2 - Test direct object access
Section titled “Step 2 - Test direct object access”Request an object that isn’t yours and see whether the resolver checks ownership:
query { user(id: 42) { id email role ssn } }Swap id to another user’s. If data comes back (or the mutation succeeds) without you being that user or an admin, it’s IDOR - same bug as REST, different transport.
Step 3 - Decode Relay global IDs
Section titled “Step 3 - Decode Relay global IDs”Relay-style APIs expose a node(id: ID!) field and opaque global IDs. These are almost always base64 of Type:primarykey:
echo -n 'VXNlcjo0Mg==' | base64 -d # -> User:42echo -n 'User:43' | base64 # -> VXNlcjo0Mw== (the next user)Decode one you legitimately hold, change the primary key, re-encode, and pass it to node(id: …). The opacity is not access control - it’s the GraphQL equivalent of the encoded-reference reversal on the mass-enumeration page.
Step 4 - Mass-enumerate with aliases (and batching)
Section titled “Step 4 - Mass-enumerate with aliases (and batching)”Aliases let one query ask for the same resolver many times, so you dump a whole table in a single request - no per-request rate limit to fight:
query { u1: user(id: 1) { email role } u2: user(id: 2) { email role } u3: user(id: 3) { email role } # ...generate up to the server's document/complexity limit}Generate the document programmatically (a short script emitting uN: user(id:N){...} lines). If the endpoint also accepts a JSON array of operations (query batching), that’s a second batching channel. Watch for depth/complexity/alias caps - those are the server-side limits you may hit.
Defense
Section titled “Defense”Same core fix as all IDOR - server-side object-level authorization in every resolver - plus GraphQL-specific hardening (disable introspection in production, cap query depth/complexity and alias/batch counts, rate-limit by operation cost). See IDOR defense.
Quick reference
Section titled “Quick reference”| Step | Action |
|---|---|
| Map | __schema introspection; InQL/Voyager; field-suggestion leaks if off |
| Direct | user(id:N) / updateUser(id:N,...) - swap the id |
| Global IDs | base64-decode node IDs (Type:pk), change pk, re-encode |
| Mass | alias the resolver uN: user(id:N) in one document; or JSON batch |
| Escalate | mutations on others’ objects; privileged fields on your own |
Next move
Section titled “Next move”- Introspection returns the schema → enumerate id-taking queries/mutations; start direct object access
node(id:)present → decode the global IDs and enumerate by primary key- Need bulk → alias-batch in one request to dodge rate limits (mass enumeration for the extraction/parsing side)
- Mutation accepts another user’s id → account takeover / privilege escalation; see chaining
- Hardening this → IDOR defense