AppSec · Jul 3, 2026 · 6 min read

GraphQL introspection: your schema is not a secret - treat it like one

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.

01One query, full schema disclosure

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.

attacker GraphQL endpoint ──► POST /graphql {"query":"{__schema{types{name fields{name}}}}"} ◄── 200 OK {data: {__schema: {types: [User, Order, AdminConfig, ...]}}} // full map schema reveals undocumented fields: AdminConfig.resetAllUsers(confirm: Boolean!) Order.bulkDelete(ids: [ID!]!) not in the docs, not in the frontend - but real and callable
A single spec-compliant request returns the complete type system. The attacker never guesses field names.

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.

02Confirming introspection and scoring what it exposes

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.

introspection-probe.httphttp
# 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.

03The real risk: authorization drift on exposed mutations

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.

Proven - CriticalMissing authorization on mutation - /graphql - resetPassword
Step 1: introspection returns resetPassword(userId: ID!, newPassword: String!) Step 2 (session authenticated as user 1001): POST /graphql {"query":"mutation{resetPassword(userId:\"1002\",newPassword:\"nonce-62615533\")}"} ──► 200 OK {"data":{"resetPassword":true}} (cross-user write accepted) Confirmed: user 1001 modified account 1002. No ownership check present.
Oracle: differential authorization test - own user accepted, other user also accepted. No real credential changed; nonce used as the newPassword value. Severity critical - cross-account write on an authentication-critical mutation.

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.

04Disabling introspection and what to test regardless

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.

ConfigurationSchema discoverableAuth gaps exploitableRisk
Introspection ONYes - full schema in one requestYesFull surface known; gaps directly exploitable
Introspection OFFVia field guessing / batched queriesYesReduced discoverability; auth gaps remain
OFF + error maskingHarder; common names still guessableYesBetter; does not fix authorization
OFF + persisted queries onlyNoYesStrongest 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.

1
request to dump the full schema
0
writes sent during schema discovery
100%
of mutations need authorization testing regardless

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.

Evidence over heuristicsReachable beats foundMonitor → Block: rolling out a gate without slowing teamsIaC-grounded threat modelingAir-gapped AppSec with no phone-homeOne platform vs three tools
See it on your own app →