By the SurfaceCheckr team. Every finding maps to a check we run in production - see our methodology.

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.

A note on Apollo Server specifically, since it's the most common setup: it already disables introspection when NODE_ENV is set to production, so the usual way it leaks is forgetting to set NODE_ENV=production in the live environment, or explicitly forcing introspection: true. Other servers (raw graphql-js, express-graphql, Yoga) leave it on by default regardless of environment, so the fix is to set it explicitly.

// Leaking: introspection forced on, or NODE_ENV not set to production
const server = new ApolloServer({
schema,
introspection: true, // (or NODE_ENV unset, so the prod default never kicks in)
});
Set introspection explicitly off in production; keep it in dev. On Apollo, also make sure NODE_ENV is actually 'production' in your live env.

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.

External, read-only scan. We only request public URLs - never log in, never send attack traffic.