Skip to content

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 id
query { 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-encode
query { 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.

  • One endpoint, many objects. /graphql resolves 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), __schema lists 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.
  1. Try introspection. If it’s on, pull the full type/field/query/mutation list; a tool like GraphQL Voyager or InQL (Burp) renders it.
  2. 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.
  3. Note every query/mutation that takes an id/uuid/object-reference argument - those are your IDOR candidates.

Request an object that isn’t yours and see whether the resolver checks ownership:

query { user(id: 42) { id email role ssn } }
mutation { updateUser(id: 42, input: { email: "[email protected]" }) { id } }

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.

Relay-style APIs expose a node(id: ID!) field and opaque global IDs. These are almost always base64 of Type:primarykey:

Terminal window
echo -n 'VXNlcjo0Mg==' | base64 -d # -> User:42
echo -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.

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.

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
  • 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