Testland
Browse all skills & agents

introspection-attack-surface-reference

Pure-reference catalog of GraphQL introspection as an attack surface and the production-deployment controls for it. Covers what introspection exposes (every type, field, directive, deprecation, description via __schema / __type), Apollo Server's default behaviour (introspection: false when NODE_ENV=production), the `hideSchemaDetailsFromClientErrors: true` companion setting (strips 'did you mean' suggestions), Yoga / Mercurius / Hasura equivalents, query-depth + query-cost limits, persisted-query allowlisting as the strongest mitigation, and the testable behaviours each control creates. Use when designing the production-safety posture of a GraphQL server or auditing an existing deployment.

Install with skills.sh (any agent)

npx skills add testland/qa --skill introspection-attack-surface-reference
View source

introspection-attack-surface-reference

Overview

Pure-reference catalog of GraphQL introspection as a production attack surface and the controls available to mitigate it. Per Apollo Server docs (apollographql.com/docs/apollo-server/api/apollo-server (opens in new window)): "Introspection enables important development tools... However, this capability also allows attackers to explore your API structure."

Consumed by the per-framework GraphQL testing skills. This skill does not execute anything.

When to use

  • Designing the production-deployment posture for a GraphQL server.
  • Auditing an existing deployment - is introspection disabled, and is the test suite proving it?
  • Writing tests that gate the production introspection setting.
  • PR review where someone proposes enabling introspection in production for "debugging."

What introspection exposes

Per the GraphQL spec, the __schema and __type queries return:

FieldReveals
__schema.typesEvery type defined (including internal)
__schema.queryType / mutationType / subscriptionTypeOperation roots
__schema.directivesCustom directives + arguments
__type(name: "X").fieldsEvery field on type X - names, types, deprecation
__type.fields.argsArgument names, types, default values
__type.descriptionSchema docstrings (often have internal context)

A single query: query { __schema { types { name fields { name type { name } } } } } returns the entire API surface in JSON. An attacker uses this to:

  1. Enumerate every operation (often inferring auth gaps from missing Mutation field-level checks).
  2. Map type relationships → infer the data model.
  3. Find deprecated fields (often kept for backwards compat, often less hardened).
  4. Find internal-looking types/fields (AdminUser, _internal, __legacy) → high-value targets.

Production controls

Disable introspection

Per apollographql.com/docs/apollo-server/api/apollo-server (opens in new window):

const server = new ApolloServer({
  typeDefs,
  resolvers,
  introspection: false,
});

Apollo defaults to introspection: true except when NODE_ENV === 'production'. Explicit is safer:

introspection: process.env.NODE_ENV !== 'production'

Hide "did you mean" suggestions

Apollo Server also recommends hideSchemaDetailsFromClientErrors: true - without it, a typo ({ usre { id } }) returns "Did you mean 'user'?", leaking field names even with introspection disabled.

Per-framework controls

FrameworkDisable introspection
Apollo Serverintrospection: false constructor option
GraphQL YogamaskedErrors: { ... } + use @graphql-yoga/plugin-disable-introspection (opens in new window)
Mercurius (Fastify)graphiql: false + routes: false (or per-route)
HasuraHASURA_GRAPHQL_DISABLE_INTROSPECTION_PUBLIC_API=true
Pothos (schema builder)Configure the underlying server (Yoga/Apollo)

Mitigations beyond disabling

Disabling introspection is the start, not the end. A determined attacker can still field-fuzz with educated guesses. Additional controls:

Query-depth limiting

Reject queries with depth > N. Prevents the "recursive friendship-graph fan-out" DoS:

query { user { friends { friends { friends { friends { ... } } } } } }

Use graphql-depth-limit (Apollo) or @envelop/depth-limit (Yoga). Typical limit: 5-7 for client- facing schemas; higher for trusted internal.

Query-cost analysis

Assign a cost to each field; reject queries with total cost > N. Catches breadth-attacks where depth is low but the multiplicative fan-out (e.g., posts { comments { likes { user { profile } } } }) is enormous.

graphql-cost-analysis (Apollo) or @envelop/operation-complexity (Yoga).

Field-level authorisation

Per-field @auth directives or resolver-level checks. Even with introspection on, attackers can't read what they can't access. Belt-and-suspenders with disabling.

Persisted-query allowlisting

The strongest mitigation. Only pre-registered query hashes execute; ad-hoc queries (including introspection probes) are rejected at the request layer. See persisted-query-strategy-reference.

Testable behaviours

Each control creates a test:

ControlTest
Introspection disabled in prodPOST /graphql { __schema { types { name } } } → 400 / "introspection is not allowed"
hideSchemaDetailsFromClientErrorsTypo query → no "did you mean" in response
Query-depth limitConstruct depth-N+1 query → error "exceeds maximum operation depth"
Query-cost limitConstruct high-cost query → error "exceeds maximum operation cost"
Persisted-query allowlistSubmit non-allowlisted hash → PERSISTED_QUERY_NOT_FOUND (per persisted-query-strategy-reference)

These tests must run against the production configuration - running them against NODE_ENV=test may give false positives if test config differs from prod.

Anti-patterns

Anti-patternWhy it failsFix
introspection: true in production for debuggingPermanent attack surface; "temporary" debug becomes defaultUse BFF + protected admin schema for debug; never main API
Trusting Apollo's NODE_ENV defaultOne mis-set env var = leakExplicit process.env.NODE_ENV !== 'production' or hard false
Disable introspection but allow GraphiQL UIGraphiQL queries introspectDisable both
Disable introspection without testingConfig drift; deploys re-enable silentlyProduction smoke test asserts 400 on __schema query
Field-level auth on Query but not MutationMutations often the more sensitiveAudit every field, both directions
No depth / cost limit, introspection disabledBrute-force fuzzing still worksLayer the mitigations
Persisted queries enabled but PERSISTED_QUERY_NOT_FOUND retries succeedEffectively no allowlistSee persisted-query-strategy-reference Mode 3
Production schema includes description with internal contextSchema disclosure even via partial introspectionSanitise descriptions; treat as user-visible

Limitations

  • Disabling introspection doesn't hide the schema entirely. Field guessing still works. Treat introspection-disable as defence-in-depth, not the whole defence.
  • Persisted queries break ad-hoc clients. Internal admin tools that build queries on the fly need a separate endpoint.
  • Codegen / Schema-first dev workflows. Need a way to fetch the schema in CI without exposing it publicly: schema-push to an internal artifact registry (Apollo Studio, Hive).
  • GraphiQL on staging. A common leak - staging schema often mirrors prod; staging URL discovered → schema disclosed.
  • __typename always works. Even with introspection off, the meta-field __typename returns the type name in responses - some inference still possible.

References

Related skills

apollo-server-tests

Wraps Apollo Server testing patterns: `server.executeOperation()` (in-process, no HTTP), `supertest` against an ephemeral-port HTTP server (port 0), context injection via the `contextValue` second-argument, and assertion patterns for response shape + errors. Includes the production-config gates testable through this skill - introspection-disabled, persisted-query mode, hideSchemaDetailsFromClientErrors. Use when writing tests for an Apollo Server v4+ GraphQL service.

graphql-complexity-limit-tester

Crafts over-limit depth and complexity queries then asserts rejection before execution, verifying that graphql-depth-limit, graphql-cost-analysis, and graphql-armor (max-depth / cost-limit / max-tokens plugins) are actually enforced and not just configured. Use when auditing a GraphQL service for DoS exposure after depth or cost limits have been added as mitigations, or when adding tests that prove the limits in CI before a production deployment.

graphql-n-plus-one-remediation

Traces a GraphQL resolver tree to locate the N+1 pattern (one parent query returns N rows, then a child field resolver fires once per row), classifies every child field resolver as safe or N+1 risk, and applies one of three fixes: per-request DataLoader batching, eager projection in the parent resolver, or selection-set-aware prefetch. Use when a list-returning resolver is added or changed in review, when a connection-pool exhaustion or slow-query alert traces back to GraphQL traffic, or when a resolver trace shows a child field resolved once per parent row.

graphql-subscription-test-author

Authors GraphQL subscription resolver test suites over graphql-ws (WebSocket) and graphql-sse (Server-Sent Events) transports: subscribe to event streams via the async-iterator API, assert emitted data shape and sequence, verify connection lifecycle and protocol close codes, and validate auth-on-connect (connectionParams / authenticate callback) plus resolver-level pubsub trigger logic. Use for real-time subscription operations; not for queries or mutations - for those use apollo-server-tests, graphql-yoga-tests, or mercurius-tests.

graphql-yoga-tests

Tests a GraphQL Yoga server (the-guild.dev runtime) with `yoga.fetch()` for in-process, no-network request simulation of queries and mutations, `@graphql-tools/executor-http` for subscription and incremental-delivery (streaming) tests, auth-header pass-through, and production-config gates for disabled introspection and persisted operations. Use to test a GraphQL Yoga server, write Yoga query, mutation, or subscription tests, or check its production plugin config; for a different runtime harness use apollo-server-tests, mercurius-tests, or hasura-tests instead, not this skill.

hasura-tests

Wraps Hasura GraphQL Engine testing patterns: docker-compose test instance, the metadata API for declarative schema/permission setup, x-hasura-role and x-hasura-user-id session headers for role-based permission tests, the v1/graphql endpoint via curl/HTTPie/native HTTP clients, and the role-by-table-by-operation permission-matrix pattern. Use for a metadata-driven Hasura engine where row-level permissions dominate; for a code-first server runtime harness use graphql-yoga-tests, apollo-server-tests, or mercurius-tests instead, not this skill.

mercurius-tests

Wraps Mercurius (Fastify GraphQL plugin) testing patterns: `app.inject()` for HTTP-layer simulation without spinning up a network listener, plugin-registration setup (await app.register(mercurius, { schema, resolvers, graphiql: false })), production-config gates (graphiql: false; jit threshold; query depth limits via fastify-rate-limit + complexity), and the per-test app lifecycle (app.close() in afterEach). Use when writing tests for a Fastify + Mercurius GraphQL server.

persisted-query-strategy-reference

Pure-reference catalog of GraphQL Persisted Query strategies: the Apollo APQ SHA-256 hash protocol, the PersistedQueryNotFoundError retry flow, the `extensions.persistedQuery` payload, GET-vs-POST and CDN-cache implications, and the three operation modes (auto-register, strict allowlist, hybrid). Use to design or audit a server's persisted-query request layer. This strategy reference emits no tests; to author the runtime tests use apollo-server-tests, graphql-yoga-tests, or mercurius-tests, and for introspection lockdown see introspection-attack-surface-reference.

pothos-builder-tests

Wraps Pothos GraphQL schema-builder testing patterns: testing the SchemaBuilder output (lexicographicSortSchema + printSchema for snapshot tests), testing resolvers via the standard `graphql()` function from graphql-js (no server needed), integration with Apollo Server / GraphQL Yoga (Pothos emits standard graphql-js schemas), and code-first builder unit tests. Covers the SchemaBuilder API surface (queryType, mutationType, objectType, t.field, t.arg). Use when testing a Pothos-built schema before or alongside the server-runtime tests.