GraphQL Schema Reconstruction When Introspection Is Disabled
__schema returns 400. Hunters shrug and move on. That is early. Production graphs still leak field names through validation errors, suggestions, and slightly different messages for “unknown field” vs “wrong type.”
Schema reconstruction is recon, not a vuln by itself. The payout is the IDOR/BOLA you find once you know invoice(id:) exists.
Oracles you can use
Try a nonsense field on the root:
{ definitelyNotAField }
Some engines reply with “Did you mean user?” Others list allowed operations in debug mode. Turn off pretty clients and read raw JSON. Compare error bodies for FIELD_ERROR vs type mismatches — that tells you a field exists even when suggestions are off.
Next, probe types you already saw in the SPA bundle (CreateOrderInput, WorkspaceMember). Frontend JS is often a partial schema dump with friendlier names than introspection. Source maps, if shipped, accelerate this; treat them as a gift, not a requirement.
Wordlists help for common roots: user, users, node, viewer, me, search, admin. Keep noise low on production. One thoughtful probe per second beats a 10k wordlist spray.
Tooling without cargo-culting
Clairvoyance-style tools automate field discovery via error feedback. Use them on targets that allow active recon, throttle hard, and prefer staging. Blind blasting __typename on every path is how you get rate-limited into irrelevance.
Manual loop that still works:
- Confirm GraphQL with
{__typename}. - Enumerate root fields via errors / JS bundle.
- For each object type, guess scalars then nested objects.
- Note arguments (
id,uuid,slug) — those drive BOLA tests later. - Capture mutations separately; write paths pay more than reads.
Export your working SDL (even a messy one) into notes. Next week’s hunt starts faster when you are not rediscovering viewer from scratch.
From map to bugs
Once you have candidates:
- Swap IDs across two accounts on every
*ByIdfield - Try admin-looking fields without admin role
- Batch queries that fetch many objects by ID
- Check mutations that accept client-supplied owner/tenant IDs
- Test whether
node(id:)global IDs decode to other tenants’ objects
Reconstruction without a follow-up auth test is a notebook entry. Reconstruction plus cross-user order(id:) is a report.
Limits and ethics
Do not treat suggestion oracles as a free dictionary attack against unrelated services. Stay in scope. If introspection is disabled and errors are generic and the SPA ships no schema hints, say so and move to REST.
Disable introspection in prod, return generic validation errors, prefer persisted queries, and still enforce object-level auth — hiding names is not authorization. I still rebuild schemas on hardened targets; I just expect more time in the JS bundle than in error messages.
PortSwigger’s GraphQL material assumes you will work with or without introspection. Rebuild just enough schema to name the operation in your PoC.