What an attacker sees before they touch your site

Is your GraphQL endpoint handing strangers your entire schema through introspection?

GraphQL has a feature that makes the whole ecosystem work: introspection. Ask a GraphQL endpoint the right query and it describes itself, every type, every field, every argument, every mutation, in full. That is how your IDE autocompletes, how GraphiQL renders its docs panel, how client codegen knows your schema. It is genuinely useful, and it is on by default in most setups.

That is also the problem. Introspection does not check who is asking. Left enabled on a production endpoint, the same query that powers your dev tooling hands a stranger the complete, machine-readable blueprint of your API, including the parts you never linked, never documented, and assumed nobody would find.

What an attacker reads off the schema

Normally the slow part of attacking an API is mapping it: guessing field names, working out which arguments each query takes, finding the mutations nobody advertises. Introspection collapses that work to a single request. The response is your entire schema in a clean, structured form an attacker can feed straight into tooling.

request
POST /graphql HTTP/1.1 Host: api.yoursite.com {"query":"{ __schema { types { name fields { name } } } }"}
response
HTTP/1.1 200 OK
Content-Type: application/json
{"data":{"__schema":{"types":[
{"name":"User","fields":[{"name":"email"},{"name":"isAdmin"}]},
{"name":"Mutation","fields":[{"name":"deleteUser"},{"name":"impersonate"}]}]}}}
One introspection query returns every type, field, and mutation, including impersonate and deleteUser.

The schema names the dangerous mutations explicitly, the impersonate, the deleteUser, the setRole, the internal admin operations that have no business being discoverable by an anonymous caller. It exposes fields a query might over-return, an isAdmin, a passwordHash, a ssn sitting on a type the UI never displays. It documents the exact arguments each operation expects, so an attacker knows precisely what to send. Mapping that would normally take patient probing; introspection prints it.

Turning introspection off is not quite enough

Disabling introspection is the right first move, but many GraphQL servers have a quieter leak that survives it: field suggestions. Send a query with a slightly-wrong field name and the server, trying to be helpful, replies "Did you mean emailAddress?" That single hint confirms a field exists and reveals its real name. Repeat it against a list of guesses and an attacker can reconstruct a meaningful chunk of the schema field by field, even with introspection fully disabled.

0:00Attacker POSTs an introspection query to /graphql
0:10Gets the full schema: types, fields, mutations
0:30Finds an unlinked admin mutation in the dump
1:00Builds the exact request to probe it
The schema is the map. With it, every other attack on the API starts halfway done.

So the schema being readable is the finding, and there are two ways it leaks: the introspection query answering, and field suggestions confirming names one at a time. A production GraphQL endpoint should do neither for an anonymous caller.

Disable introspection and suggestions in production

The clean fix is to turn both off in your production configuration, while keeping them on in development where they earn their keep.

// Apollo Server: introspection on by default
const server = new ApolloServer({
schema,
// introspection enabled, suggestions enabled
});
Disable introspection and field suggestions in the production config; keep them in dev.

Disable introspection in production so the __schema query returns nothing to an anonymous caller, and strip field suggestions from error responses so a wrong field name does not confirm the right one. If your tooling or partners genuinely need the schema in production, gate it behind authentication rather than leaving it open, the same way you would gate a public Swagger UI rather than serve your API docs to the world. The goal is the same: the full map of your API should not be the easiest thing on your domain to read.

This is checkable from outside, because if introspection is on, your own front-end's GraphQL traffic often carries the proof. SurfaceCheckr watches the API responses your page fires during a real-browser render and flags a __schema or __type payload coming back, or a "Did you mean" field suggestion in an error, the two signals that your schema is readable. It reads the responses your page already requested and stays passive: it does not send its own introspection query, replay traffic, or probe field names, because forcing the endpoint to talk is active testing and we don't do that. What it can confirm is whether the blueprint is already in the open. Whether it should be is one config line away.

Find it before someone else does.

Paste your domain. The grade and issue count are free, and you'll see in a couple of minutes exactly what's reachable from outside.