Leaving introspection enabled in production hands every attacker a self-documenting map of your API surface - including the mutations you never published.
GraphQL's introspection system exists for developer tooling: send one __schema query and the API returns every type, field, argument, mutation and subscription it exposes. In a local environment that convenience is real. Left enabled in production, it hands every attacker a self-documenting map of your entire API surface - including mutations you never published and admin fields you assumed were undiscoverable.
A GraphQL introspection query is a valid, spec-compliant request. No special header is required, and no authentication bypass is needed: if introspection is enabled and the endpoint is reachable, any HTTP client can ask for the complete schema and receive it. The response names every queryable type and its fields, every mutation with all input arguments, and every subscription. It also surfaces deprecated fields - a useful clue about operations the team stopped documenting without removing from the running server.
The distinction that matters for severity is not 'can the attacker enumerate types' but 'what can they do with the fields they find'. Introspection reveals the surface; the findings that follow it - authorization gaps on exposed mutations, BOLA on ID-typed arguments - are what decide real impact.
The confirming probe for introspection is a single read-only __typename or condensed __schema query. No mutation runs. No record is written or modified. The confirming artifact is the response itself: if the server returns type information, introspection is confirmed enabled. Severity at this stage ranges from informational to medium, depending on what the returned schema reveals.
# step 1 - lightweight check POST /graphql Content-Type: application/json {"query": "{__typename}"} HTTP/1.1 200 OK {"data":{"__typename":"Query"}} # introspection responding # step 2 - full schema read (no side effects) POST /graphql {"query":"{ __schema { queryType{name} mutationType{name} types{name kind fields{name args{name type{name kind ofType{name kind}}}}} } }"} HTTP/1.1 200 OK # returns all types, mutations, subscriptions, deprecated fields
Once the schema is in hand every field name and argument is a confirmed, targetable surface. Mutations that accept ID arguments are queued for object-level authorization testing. Mutations whose names suggest privileged operations are probed for missing access controls. Type names that appear nowhere in the published API documentation are flagged as shadow fields - the GraphQL equivalent of a shadow API endpoint.
Introspection does not cause the breach - it reveals the surface that was already there. The highest-impact findings follow the schema. A mutation named resetPassword(userId: ID!, newPassword: String!) that accepts any user ID without an ownership check is an account takeover; introspection named the argument shape and DAST confirmed the missing check by submitting a benign nonce for a second user ID and observing whether the operation was accepted or rejected.
The same probe pattern applies to admin mutations the schema names but the application layer was supposed to protect with a role check. A mutation that accepts forged input without verifying the caller's role is a privilege escalation regardless of whether introspection was the path that revealed it. Disabling introspection removes the signpost, not the gap.
The immediate remediation for production introspection exposure is a one-line configuration change on the server: Apollo Server, GraphQL Yoga, Strawberry and Hot Chocolate all expose an option to reject introspection queries outside development mode. This is a defense-in-depth measure, not a substitute for authorizing every mutation and subscription independently.
| Configuration | Schema discoverable | Auth gaps exploitable | Risk |
|---|---|---|---|
| Introspection ON | Yes - full schema in one request | Yes | Full surface known; gaps directly exploitable |
| Introspection OFF | Via field guessing / batched queries | Yes | Reduced discoverability; auth gaps remain |
| OFF + error masking | Harder; common names still guessable | Yes | Better; does not fix authorization |
| OFF + persisted queries only | No | Yes | Strongest posture; custom queries rejected at the gateway |
Field-guessing attacks against a disabled endpoint still succeed against short or common field names, especially when error responses reflect the unknown field name back to the caller. Disabling introspection removes the convenience; it does not remove the need to test authorization on every operation the server actually accepts. apPosture tests both layers: introspection state and per-mutation authorization checks, because disabling introspection while leaving authorization open is a false sense of closure - the gap moves from discoverable to slightly harder to find.
Introspection is a diagnostic tool that doubles as a reconnaissance tool. The right posture in production is not to hope attackers do not know about __schema - it is to disable it, mask detailed error responses, apply query depth limits, and then test authorization on every operation the schema declares, whether or not introspection could have surfaced it first.