How does GraphQL introspection work?
The GraphQL specification makes every schema self-describing. Any GraphQL server exposes the meta-fields __schema and __type on the root query type, and __typename on every object, so a client can ask the server what it supports.
The smallest useful introspection query lists every type:
POST /graphql HTTP/1.1
Host: api.example
Content-Type: application/json
{"query": "{ __schema { types { name } } }"}HTTP/1.1 200 OK
Content-Type: application/json
{"data": {"__schema": {"types": [
{"name": "Query"}, {"name": "Mutation"}, {"name": "Project"},
{"name": "ProjectInput"}, {"name": "AdminSettings"}, {"name": "__Schema"}
]}}}Tools send the full query that GraphQL client libraries use, usually named IntrospectionQuery. Its opening, as generated by graphql-js's getIntrospectionQuery(), shows how much it asks for:
query IntrospectionQuery {
__schema {
queryType { name }
mutationType { name }
subscriptionType { name }
types { ...FullType }
directives { name description locations args { ...InputValue } }
}
}
fragment FullType on __Type {
kind name description
fields(includeDeprecated: true) {
name description
args { ...InputValue }
type { ...TypeRef }
isDeprecated deprecationReason
}
inputFields { ...InputValue }
interfaces { ...TypeRef }
enumValues(includeDeprecated: true) { name description isDeprecated deprecationReason }
possibleTypes { ...TypeRef }
}The result is the whole schema as JSON: every query, every mutation, every argument and its type, including deprecated fields the UI stopped using. Paste it into GraphQL Voyager or InQL and you have a browsable map.
Introspection is not a vulnerability by itself. Public APIs such as developer platforms leave it on deliberately. It becomes a finding when the API is meant only for the organization's own clients, because it removes the guesswork from attacking the fields that matter.
Why does it matter to an attacker?
It turns a black-box API into a documented one. A tester reads the schema for mutations with names like deleteUser, setRole or updateAdminSettings, and for queries that take an id argument, then tests each for missing authorization.
That makes introspection the reconnaissance step for broken function level authorization (admin mutations reachable by members) and broken object level authorization (project(id:) returning other tenants' objects). It also exposes fields that were never wired into the UI, which makes it a source of shadow API surface inside a known endpoint.
Two public CVEs show the issue in real products, both confirmed on NVD:
| CVE | Product | Issue | Fixed in |
|---|---|---|---|
| CVE-2023-47643 | SuiteCRM | Introspection enabled without authentication, exposing the full schema | 8.4.2 |
| CVE-2024-37155 | OpenCTI | Regex check meant to block introspection bypassed by removing whitespace and line breaks | 6.1.9 |
What does disabling introspection protect, and what doesn't it?
Disabling introspection stops the one-request schema dump. It does not hide the schema from a determined tester, and it does nothing for the authorization bugs the schema would have revealed.
What still leaks with introspection off:
- Field suggestions. Many servers answer a mistyped field with a hint. Query
{ projct { id } }and the error readsCannot query field "projct" on type "Query". Did you mean "project"?. Clairvoyance automates this: it sends wordlists of candidate field names and rebuilds the schema from the suggestions and errors, producing introspection-format JSON. - Client code. Web bundles and mobile apps contain every query and mutation the client sends, often with fragments that name more fields.
- Weak blocking logic. String and regex filters are fragile because GraphQL ignores whitespace and commas. PortSwigger shows a filter matching
__schema {bypassed with a newline or comma after__schema. CVE-2024-37155 was the reverse: a regex that expected line breaks, defeated by removing them. Some servers only block introspection on POST, so try GET or a form-encoded body. __typename. It stays available and confirms a GraphQL endpoint exists.
How do you test for it?
- Find the endpoint. Try
/graphql,/api/graphql,/v1/graphql,/graphql/consoleand/query. Send{"query": "{ __typename }"}; a{"data": {"__typename": "Query"}}reply confirms GraphQL. - Send the full introspection query unauthenticated, then with a low-privilege session. Some servers only allow it for signed-in users, which may still be broader than intended.
- If blocked, try bypasses: whitespace or commas after
__schema, GET with aqueryURL parameter,application/x-www-form-urlencodedbodies, and a__type(name: "Query")query. - If still blocked, check suggestions. Send deliberately misspelled field names. A "Did you mean" reply means Clairvoyance can recover the schema.
- Mine the schema. List mutations and ID-taking fields, then test each with two accounts and two roles.
How do you fix it?
Turn introspection and suggestions off for private APIs, and put the real protection in resolver-level authorization and query limits.
Apollo Server disables introspection by default when NODE_ENV is production. Setting it explicitly avoids surprises when that variable is missing, and hideSchemaDetailsFromClientErrors strips "Did you mean" hints (it defaults to false):
import { ApolloServer } from "@apollo/server";
const server = new ApolloServer({
typeDefs,
resolvers,
introspection: false,
hideSchemaDetailsFromClientErrors: true,
});With plain graphql-js, add the NoSchemaIntrospectionCustomRule validation rule. For Apollo Server, GraphQL Yoga and Envelop-based servers, GraphQL Armor adds a Block Field Suggestions plugin plus Max Depth, Max Aliases, Max Directives, Max Tokens, Character Limit and Cost Limit plugins against abusive queries.
Beyond that:
- Authorize in every resolver, including mutations and nested fields. Hiding the schema doesn't enforce anything.
- Use persisted queries or an operation allowlist for first-party clients, so the server only runs operations it knows.
- Leave introspection on in development and staging behind authentication, where tooling needs it.
A WAF rule that blocks __schema is the same brittle filter behind CVE-2024-37155.
[ Sources ]
- GraphQL specification (October 2021): Introspection
- graphql.org: Introspection
- Apollo Server API reference: introspection and hideSchemaDetailsFromClientErrors
- PortSwigger Web Security Academy: GraphQL API vulnerabilities
- Clairvoyance: schema recovery with introspection disabled
- NVD: CVE-2024-37155 (OpenCTI introspection check bypass)
- NVD: CVE-2023-47643 (SuiteCRM unauthenticated introspection)
Written by Parameter · Last reviewed

