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
- Enumerate the schema (introspection or clairvoyance for disabled introspection)
- For every Query field, test with another tenant’s IDs / your account vs a peer’s
- For every Mutation field, test injection of unexpected input fields (role, isAdmin, tenantId, verified)
- For Connection / edge fields, traverse beyond the entry point — what fields are reachable on related types?
- Test batched queries crossing tenant boundaries
- 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.
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.