GraphQL is great. It lets you request exactly the data you need, nothing more, nothing less. Efficient. Flexible. Elegant.
It also comes with a feature called introspection — the ability to query the API for a complete description of itself. Every type. Every query. Every mutation. A full map of everything the API can do.
In development, this is incredibly useful. You can explore the schema, understand the data model, build queries interactively.
In production, with no authorization on the admin mutations, introspection is a detailed treasure map to everything you’re not supposed to touch.
The Discovery
Redacted-app.com had a GraphQL endpoint at /api/graphql. I sent the standard introspection query:
{
__schema {
types {
name
fields {
name
}
}
}
}
Got back the full schema. 847 types. 203 queries. 94 mutations.
I filtered the mutations by name. Alphabetically. Looking for anything interesting.
Found:
deleteUser
exportAllUserData
grantAdminRole
impersonateUser
listInternalAuditLogs
resetUserPassword
revokeAdminRole
suspendUser
updateBillingPlan
impersonateUser. grantAdminRole. exportAllUserData.
These are admin mutations. They should require admin authentication. I was a regular user.
I called impersonateUser:
mutation {
impersonateUser(userId: "some-other-user-id") {
success
sessionToken
}
}
Response:
{
"data": {
"impersonateUser": {
"success": true,
"sessionToken": "eyJ..."
}
}
}
A session token. For another user. Given to me because I asked nicely in GraphQL syntax.
The Scope of It
I tested each admin mutation. All of them worked without admin credentials. All of them.
exportAllUserData returned a download URL for a zip file containing the complete user database export — emails, names, account details, subscription information for all users.
I did not download it. The URL existing was sufficient proof.
grantAdminRole — I didn’t run this. I wasn’t going to grant myself admin in their production system.
listInternalAuditLogs — returned the last 100 audit log entries. Real admin actions, timestamps, user IDs.
The authorization layer that was supposed to protect these mutations: completely absent. The schema correctly described them as admin operations. The execution layer forgot to check.
The Report
Title: GraphQL Introspection Enabled in Production — All Admin Mutations Accessible Without Authorization — Confirmed impersonateUser and exportAllUserData
Severity: High
Impact: GraphQL introspection reveals 9 admin-only mutations that execute without authorization checks. Demonstrated: successful user impersonation (receiving valid session token for arbitrary user), confirmed existence of full user data export endpoint. Potential impact includes account takeover of any user, export of complete user database, modification of user billing plans, and access to internal audit logs.
Fix:
- Disable introspection in production (not a security fix by itself, just reduces reconnaissance)
- Add authorization middleware that checks admin role before executing any mutation with admin scope — this is the actual fix
- Implement field-level authorization rather than relying on mutation naming conventions
Triaged High. Fixed in 36 hours. Bounty: $$$
The Introspection Misconception
Disabling introspection is the first thing most teams do when they hear “GraphQL security.” It’s not wrong. Reducing information exposure is good.
But it’s not a fix. It’s making the lights dimmer.
The authorization bug exists whether or not introspection is enabled. A determined attacker who knows common GraphQL mutation naming conventions will find impersonateUser eventually. Introspection just made it faster.
The fix is authorization. Always. Introspection is a separate concern.
Reported through the official bug bounty program. Impersonation was demonstrated using a test account I controlled as the target. No real user accounts were impersonated. The export URL was noted but not accessed. Domain redacted per responsible disclosure norms.