GraphQL Authorisation Bypass: The Deep-Dive

Manish Garg
Manish Garg Associate of (ISC)² · RingSafe
Apr 25, 2026
4 min read

Last updated: April 26, 2026

GraphQL’s most consequential vulnerability class isn’t injection — it’s authorisation bypass. Field-level resolvers that don’t validate the requesting user’s right to read or modify each piece of data create a rich attack surface that traditional REST authentication patterns don’t have. This article covers the GraphQL authorisation bypass patterns we find on Indian SaaS audits, the systematic test methodology, and the resolver-level fix that closes the entire bug class.

The mental model

REST APIs typically have endpoint-level authorisation: /api/users/123 is checked against the requestor’s permission to access user 123. GraphQL queries one endpoint and selects fields from a schema. Authorisation needs to happen per-field per-resolver, not per-endpoint.

If the application authorises the request once at the GraphQL endpoint and trusts every resolver thereafter, every resolver becomes an authorisation bypass candidate.

The bypass patterns

1. Field-level over-exposure

query {
  me {
    id
    email
    # Some apps expose internal fields:
    isAdmin
    apiToken
    paymentMethods { last4 }
  }
}

Many GraphQL APIs expose every field of a type because the schema doesn’t distinguish between user-readable and admin-readable fields. An authenticated user reads their own user object — but the user object exposes apiToken or passwordHash field that should only be admin-readable.

2. Resolver-level IDOR

query {
  user(id: 999) {
    email
    phoneNumber
    address
  }
}

If the user resolver doesn’t verify the requestor’s right to read user 999, you have IDOR. Common because resolvers are often direct database queries.

3. Mutation field injection

mutation {
  updateUser(input: {
    id: 999,
    email: "[email protected]",
    role: "admin",       # Often accepted despite being non-user-modifiable
    isVerified: true     # Mass assignment
  }) { id }
}

If the mutation resolver uses input fields directly without an allow-list, role escalation in one query.

4. Connection / edge traversal

query {
  organisation(id: 1) {
    members {
      edges {
        node {
          email
          # Connections often expose more than the resolver intended
          permissions
          internalNotes
        }
      }
    }
  }
}

Pagination connections often expose every field of related types. The resolver authorising “you can see this organisation” doesn’t authorise “you can see every member’s internal notes.”

5. Tenant isolation bypass via batched queries

POST /graphql
[
  { "query": "{ organisation(id: 1) { name } }" },
  { "query": "{ organisation(id: 2) { name } }" },
  { "query": "{ organisation(id: 3) { name } }" }
]

If the auth middleware checks the request once but resolvers don’t enforce tenant isolation per-query, batching crosses tenant boundaries.

6. Subscription authorisation gaps

GraphQL subscriptions (WebSocket-based real-time updates) often have weaker authorisation than queries / mutations. If the WebSocket handshake authorises but the subscription resolver doesn’t filter events by tenant, the user receives every event in the system.

Systematic testing

  1. Enumerate the schema (introspection or clairvoyance for disabled introspection)
  2. For every Query field, test with another tenant’s IDs / your account vs a peer’s
  3. For every Mutation field, test injection of unexpected input fields (role, isAdmin, tenantId, verified)
  4. For Connection / edge fields, traverse beyond the entry point — what fields are reachable on related types?
  5. Test batched queries crossing tenant boundaries
  6. Test subscriptions for cross-tenant event delivery

Tools that help: InQL (Burp extension), graphql-cop, graphql-voyager for schema visualisation, Burp’s Autorize with two-session GraphQL workflow.

The fix — field-level authorisation

Authorise at the resolver level, not the endpoint level. Every resolver checks: can this user read/write this field on this object?

Frameworks supporting this:

  • graphql-shield (Node.js) — middleware that wraps resolvers with permission rules
  • Apollo Server directives — schema directives like @auth(requires: ADMIN)
  • Hasura permissions — declarative row + column-level policies in schema
  • Custom resolver wrappers — every resolver passes through a permission check before executing

Architectural patterns:

  • Tenant ID always in the resolver context, never trusted from input
  • Database queries scoped by tenant ID at the ORM/DB layer
  • Field-level allow-lists for mutations (no mass assignment)
  • Subscription filters using the same authorisation logic as queries

Detection

  • GraphQL request logging — full query, variables, response size, response time per request
  • Anomaly detection — user accessing unusually large response sizes (cross-tenant query indicator)
  • Per-resolver execution metrics — sudden spike in a specific resolver from a single user (enumeration)

Compliance angle

  • OWASP API Top 10 API1 / API3 / API5 — GraphQL maps to all three (auth, excessive data exposure, function-level auth)
  • DPDP §8(5) — GraphQL exposing personal data via authorisation gaps fails reasonable security
  • RBI / SEBI — application security testing must cover GraphQL APIs

The takeaway

GraphQL authorisation is hard because the framework lets you define what data exists without enforcing who can access it. Test every resolver with cross-tenant inputs. Audit your GraphQL service against the patterns above; the authorisation gaps you find are typically immediate findings, not theoretical vulnerabilities.

Need a real pentest?

Get a VAPT scoping call

Senior practitioner-led VAPT — not a checklist run by juniors. CVSS-scored findings, free retest, attestation letter. India's SMBs and SaaS teams.

Book VAPT scoping call Replies in 4 working hrs · India-only · Senior consultants