October 21, 2026
A Practical Guide to Hunting Bugs in GraphQL When Introspection Is Off
The schema tells you what exists. The resolvers decide who may run it.

By Raj Namdev
4 min read
The 400 error is where most hunters quit._ The schema was never the attack surface anyway โ and the paid reports prove it._
The moment everyone quits
You find the GraphQL endpoint. You fire the standard introspection query. The server hands you back a 400 Bad Request or a polite "not allowed." And the thought that follows is almost universal โ this API is locked, nothing to see here, move on.
A thread from r/bugbounty in March 2026 put numbers on that instinct. Hunters describe checking introspection, getting blocked, and moving on โ and a separate thread from July 2026 is titled, without irony, "GraphQL is my nightmare": a big target, introspection fully disabled, and dozens of JavaScript files each containing fragments of hardcoded queries.
The quit is understandable. It's also expensive. Because introspection off doesn't mean the API is untestable. It means you can't have the map. You can still walk every street.
I pulled the GraphQL reports that actually paid โ Shopify's billing IDOR at $5,000, TikTok's cross-tenant write IDOR rated high, Stripe's cross-tenant write IDOR rated high, Tools for Humanity's membership leak at $500 โ and none of them needed introspection. The bug was never in the schema. It was in what the resolvers failed to check.
Where the money actually is
Four reports, four programs, one repeated mistake.
Shopify #2207248. The operations BillDetails and BillingDocumentDownload take a BillingInvoice id. The hunter โ blaklis โ swapped the id for another merchant's invoice. The query returned the other merchant's email, full address, invoice contents, last four digits of their credit card, or their PayPal email. $5,000, medium 5.3, 187 upvotes. The full query is public: it's a completely ordinary billing query. Nothing exotic in the selection set. The id parameter was the whole bug.
TikTok #984965. The AddRulesToPixelEvents operation on ads.tiktok.com lets an advertiser add, update, and delete rules for their pixel events. One advertiser's request worked on any other advertiser's pixel events. High 7.5, 84 upvotes.
Stripe #1066203. UpdateAtlasApplicationPerson โ a user with prior admin access could add a co-founder to a Stripe Atlas application belonging to an account they used to administer. Cross-tenant write. High.
Tools for Humanity #2381816. FetchMemberships leaked names, emails, roles, and IDs of current team members to people no longer associated with the organization. $500, medium 4.4.
Read those operation names again: Fetch, Add, Update, Delete, Download. The verbs in GraphQL operation names are a testing checklist. Every one of them is an authorization boundary, and every one of these programs got it wrong somewhere.
Four techniques for the locked target
When introspection is off, you have four ways to recover ground.
1. The __type bypass
"Disabled" introspection is often a block on __schema โ the dump-everything query. The single-type query frequently still works:
{ __type(name: "Query") { fields { name } } }{ __type(name: "Query") { fields { name } } }That leaks field names one type at a time. Slower than a full dump. Same information.
2. Field suggestion harvesting
Send a typo'd field name and many servers answer with "Did you mean invoices?" โ a guessing game where the server confirms your guesses, one typo per field. The suggestion engine has no idea introspection is supposed to be off.
3. Batch queries
GraphQL accepts an array of operations in one request:
[{"query":"{ login(user:\"a\", pass:\"b\") }"},
{"query":"{ login(user:\"a\", pass:\"c\") }"}][{"query":"{ login(user:\"a\", pass:\"b\") }"},
{"query":"{ login(user:\"a\", pass:\"c\") }"}]If rate limiting counts requests instead of operations, your effective rate limit just multiplied by your array length. On auth endpoints, that's a brute-force primitive.
4. Rebuild from the JavaScript
The "nightmare" thread โ dozens of JS files with hardcoded query fragments โ is actually the standard situation, and it's solvable. Burp's traffic captures every GraphQL operation the app legitimately sends. Log in as two users, click through every feature, and your proxy history is the schema: every operationName, every variable, every mutation the frontend actually uses. Grep the JS bundles for operationName, query mutation, `gql`` โ the fragments stitch together faster than you'd expect. You don't need the server's map. You need the client's.
The test that pays: two accounts and one swapped id
Once you have operations โ from JS, from proxy history, from field suggestions โ the actual hunting is the most boring technique in web security, applied to a new surface.
- Take account A's request and replace every id-like variable with account B's โ the Shopify report is exactly this,
variables.idswapped to another merchant's invoice id. - Send mutations, not just queries. The read-leak is medium money; the write primitive is where high severity lives.
- Check the
node(id:)relay-style interface specifically. Shopify's bug flowed throughnode(id: $id)โ a generic object lookup where authorization has to be re-implemented for every type. Generic plumbing is where generic mistakes live. - Test tenant boundaries with three accounts across two organizations, not two accounts in one. Cross-tenant write is the strongest version โ both TikTok and Stripe were rated high on exactly that shape.
- Sweep every operation whose name contains
Fetch,Get,Add,Update,Delete,Download,Create. The verb is the boundary.
The Nextcloud Out-of-Office API leak from April 2026 shows the class is still paying this year. The HackerOne platform itself has paid versions โ CreateOrUpdateHackerCertification let one user delete all licenses and certifications from another user's account, 394 upvotes. When the bug bounty platform's own GraphQL produces these, no target is too "mature" to skip.
The trap: schema visible is not access
One warning before you fire up the checklist. In May 2026 a hunter published a writeup of a finding that looked like free money: a public-facing subdomain with GraphiQL exposed, no authentication, introspection enabled, and three mutations sitting in the schema. The hunter scored it 9.1, mapped four OWASP API categories, filed it.
The mutations were visible in the schema but blocked in the resolvers. The Critical became an Informative with a 50 CHF courtesy bonus.
The lesson is the mirror image of the introspection lesson: the schema tells you what exists. The resolvers decide who may run it. Attempt safe, non-destructive execution before you score anything. The two-account swap is what separates the Shopify $5,000 from the 50 CHF.
What to remember
Introspection off filters out hunters, not bugs._ The operation names are the checklist, the verbs are the boundaries, and two accounts plus one swapped id finds what the schema dump never could._
Next in this series: proving SSRF impact without crossing the line โ the exact requests that turn a DNS pingback into a critical.
Sources
- Shopify billing IDOR via BillDetails / BillingDocumentDownload โ #2207248 โ full JSON verified this session, $5,000, medium 5.3, 187 upvotes
- TikTok cross-tenant write IDOR via AddRulesToPixelEvents โ #984965 โ team and researcher summaries verified, high 7.5, 84 upvotes
- Stripe cross-tenant write IDOR via UpdateAtlasApplicationPerson โ #1066203 โ team summary verified, high, 25 upvotes
- Tools for Humanity FetchMemberships team-data leak โ #2381816 โ team summary verified, $500, medium 4.4, 91 upvotes
- HackerOne CreateOrUpdateHackerCertification delete-IDOR โ #2122671 โ dataset-cited (top IDOR list), 394 upvotes
- Nextcloud Out-of-Office API BOLA/IDOR โ #3382343 โ dataset-cited (2026 disclosed list), medium 6.5, 34 upvotes
- r/bugbounty โ "GraphQL endpoints are consistently undertested" (Mar 2026) โ community-documented techniques:
__typebypass, field suggestions, batch queries - Infosec Writeups โ GraphiQL triage lesson (May 2026) โ the schema-visibility-is-not-access case, hunter's own account