Introduction
GraphQL introspection is a powerful feature that lets clients discover the entire type system of an API at runtime. While intended for legitimate tooling (IDE autocomplete, documentation generators, and client codegen), the same capability can be weaponised by attackers to map the full attack surface in a single request.
In this guide we walk through the mechanics of introspection, show how to harvest a complete schema, and then demonstrate how that data can be turned into concrete exploitation paths such as field-level injection, privilege escalation, and authorization bypass.
Real-world incidents - from the 2021 Shopify breach to the 2023 GitLab bug bounty - illustrate that unrestricted introspection is a common mis-configuration. Mastering schema harvesting therefore moves you from a passive scanner to a proactive attacker (or defender) who knows exactly where to strike.
Prerequisites
- Intro to GraphQL Architecture & Attack Surface Overview
- Discovering GraphQL Endpoints and Enumerating Operations
- Basic HTTP request crafting (cURL, Burp Suite)
Core Concepts
At its core, GraphQL defines a schema composed of Object, Interface, Union, Enum, Scalar, and Input types. The schema is the contract between client and server, describing every query, mutation, and subscription that can be executed.
Introspection is exposed via a set of meta-fields prefixed with double underscores (__). The two most frequently used are:
__schema- returns a top-level description of the whole schema (types, directives, query root, mutation root, etc.).__type(name: <String!>)- returns details for a single named type.
Because these fields are regular GraphQL fields, they obey the same authorization rules as any other field. If the server does not explicitly block them, any unauthenticated user can retrieve the entire type system.
From a security perspective, the schema is the blueprint for every possible payload. Knowing the exact field names, argument types, and default values eliminates much of the guesswork that typical fuzzers rely on.
GraphQL introspection query syntax (__schema, __type)
The minimum query that returns a usable schema is surprisingly small. Below is a compact version that extracts the names of all types, their kind, and the fields of the query root.
{ __schema { types { kind name description fields(includeDeprecated: true) { name args { name type { name kind ofType { name kind } } defaultValue } type { name kind } isDeprecated deprecationReason } } }
}
Key points:
includeDeprecated: truesurfaces fields that are marked deprecated - often left in code for legacy reasons and a frequent source of hidden injection vectors.- The
ofTypenesting resolves wrapper types (NON_NULL,LIST) to the underlying scalar or object. - Because the response is JSON, it can be piped directly into tools like
jqfor further processing.
To fetch a single type, replace __schema with __type(name: "User") (or any other type name). This is handy when you only need a subset of the schema or when the server throttles large responses.
Using curl and GraphQL Playground to execute introspection queries
Most developers test GraphQL APIs with UI tools such as GraphQL Playground, GraphiQL, or Insomnia. Attackers can achieve the same effect with a single curl command.
curl -X POST -H "Content-Type: application/json" -d '{"query":"{ __schema { types { name kind } } }"}' GRAPHQL_ENDPOINT
Explanation:
-X POST- GraphQL endpoints typically accept POST (some also support GET with aqueryparam).- The JSON payload contains a single
queryfield. Notice the query is minified to avoid newline handling issues. - The response will be a JSON object with a
data.__schema.typesarray.
When you have access to an interactive Playground, you can paste the same query into the editor, hit Play, and download the raw response via the network tab. The advantage of Playground is that it automatically adds the Accept: application/json header and handles CSRF tokens if the server requires them.
Automated schema extraction with tools like gqlmap and GraphQL Voyager
Manually crafting introspection queries works, but dedicated utilities streamline the process, visualise the schema, and even flag suspicious fields.
gqlmap
pip install gqlmap
gqlmap -u GRAPHQL_ENDPOINT -i
The -i flag tells gqlmap to perform an introspection dump and write it to schema.json. The tool also produces a .dot file that can be rendered with Graphviz for a quick visual overview.
GraphQL Voyager
Voyager is a web-based visualiser that consumes an introspection JSON document and renders an interactive graph.
# Download the introspection result first
curl -X POST -H "Content-Type: application/json" -d '{"query":"{ __schema { types { name kind fields { name type { name kind } } } } } }"}' GRAPHQL_ENDPOINT -o schema.json
# Open Voyager locally (Node version)
npm install -g graphql-voyager-cli
voyager schema.json
Voyager’s UI highlights deprecated fields in orange and shows directives (e.g., @auth) that might hint at custom security logic.
Analyzing extracted types, fields, and arguments for attack vectors
Once you have the full schema, the next step is to turn raw data into actionable findings. Below is a systematic approach:
- Identify entry points: Look for root
QueryandMutationfields that accept user-controlled arguments. Types withStringorIDarguments are prime candidates for injection. - Spot dangerous scalars: Custom scalars like
JSON,Any, orUploadoften bypass validation layers. - Enumerate enum values: Enums restrict input but may contain values that map to internal permissions (e.g.,
ROLE_ADMIN). - Check deprecation: Deprecated fields are usually left in code for backward compatibility. They may bypass newer security checks or be undocumented.
- Map relationships: Follow
Objectfield types to understand data flow. AUsertype that contains apasswordHashfield is unlikely to be exposed, but aprofilePictureUrlthat accepts a URL string could be abused for SSRF. - Look for directives: Custom directives such as
@auth(role: "ADMIN")or@rateLimitcan be clues about where access control is enforced.
Automating steps 1-4 with jq is straightforward. Example: list all mutation fields that accept a String argument.
jq -r '.data.__schema.types[] | select(.name == "Mutation") | .fields[] | select(.args[]?.type.name == "String") | .name' schema.json
Identifying hidden or deprecated fields for injection opportunities
Deprecated fields are often left in the code base without the same level of review as live fields. Attackers can probe them with specially crafted payloads to achieve:
- SQL/NoSQL injection - if the resolver still runs raw queries.
- Command injection - when a deprecated field forwards input to a shell.
- File inclusion - fields that accept file paths or URLs.
Example: a deprecated mutation createUserLegacy that accepts a profileJson argument of type JSON. The resolver might directly deserialize the JSON into a MongoDB document, opening a NoSQL injection path.
mutation { createUserLegacy(profileJson: "{ \"$where\": \"this.password == 'pwned'\" }") { id }
}
When you see a field marked isDeprecated: true, add it to your test matrix regardless of its business relevance.
Combining schema data with subsequent injection or authorization bypass attacks
The true power of schema harvesting emerges when you chain the knowledge with other exploitation techniques.
Case 1 - Blind injection via a deeply nested input
Suppose the schema reveals an input type:
input OrderInput { productId: ID! quantity: Int! notes: String
}
The notes field is a free-form string. If the resolver builds a SQL statement like INSERT INTO notes (text) VALUES ('${notes}') without sanitisation, you can inject '); DROP TABLE users;--.
Using the previously extracted Mutation.createOrder field you can craft the payload:
mutation { createOrder(input: {productId: "1", quantity: 1, notes: "'); DROP TABLE users;--"}) { orderId }
}
Case 2 - Authorization bypass via enum abuse
If an enum AccessLevel contains values USER, MODERATOR, ADMIN, and the mutation setUserRole(userId: ID!, role: AccessLevel!) is protected only by a directive that checks @auth(role: "ADMIN") on the field, but the directive is not applied to the underlying resolver, you can simply supply ADMIN and elevate privileges.
Because the enum values are visible in the schema, you know exactly what string to send.
Practical Examples
Below is a step-by-step walkthrough of a typical pentest scenario against a vulnerable e-commerce GraphQL API.
- Harvest the schema using
curl.curl -s -X POST -H "Content-Type: application/json" -d '{"query":"{ __schema { types { name kind fields { name args { name type { name kind } } } } } }"}' GRAPHQL_ENDPOINT | jq . > full_schema.json - Identify potentially dangerous mutations.
Outputs:jq -r '.data.__schema.types[] | select(.name=="Mutation") | .fields[] | select(.args[]?.type.name=="String") | .name' full_schema.jsonaddReview,updateProfile,adminCreateProduct. - Test a deprecated field.
If the response contains the newly created product, you have a XSS injection point.curl -s -X POST -H "Content-Type: application/json" -d '{"query":"mutation { legacyAddProduct(name: \"test\", description: \"<script>alert(1)</script>\") { id } }"}' GRAPHQL_ENDPOINT - Exploit enum bypass.
If the server returnsmutation { setUserRole(userId: "123", role: ADMIN) { success } }{"data":{"setUserRole":{"success":true}}}for a normal user, you have escalated.
Each step demonstrates how the schema is the roadmap for the subsequent attack.
Tools & Commands
- curl - quick manual introspection.
- gqlmap - automated dump, endpoint fuzzing, and basic injection payloads.
- GraphQL Voyager - visual navigation of large schemas.
- jq - filter JSON responses for specific types/fields.
- Burp Suite / OWASP ZAP - intercept and replay introspection queries, modify headers, test rate-limiting.
- GraphQL-CSRF - script to test CSRF on endpoints that rely on cookies.
Defense & Mitigation
Disabling introspection in production is the most straightforward mitigation, but it can break legitimate tooling. A balanced approach includes:
- Restrict introspection to authenticated users - apply the same auth checks that protect sensitive queries.
- Rate-limit introspection queries - treat them as heavy operations; throttling prevents bulk schema harvesting.
- Strip deprecated fields from the schema - keep the production schema clean; remove unused resolvers.
- Validate and sanitise all user-controlled arguments - use parameterised queries, ORM layers, and safe deserialization for custom scalars.
- Enforce least-privilege on directives - ensure that custom @auth directives are enforced at the resolver level, not just at the schema level.
- Monitor for large
__schemaqueries - log and alert when the query size exceeds a threshold.
Framework-specific tips:
- Apollo Server - set
introspection: process.env.NODE_ENV !== 'production'. - GraphQL-Java - use
GraphQLSchema.Builder#disableIntrospection()for production builds. - Express-GraphQL - pass
graphiql: falseand custom validation rules to block__schemafields.
Common Mistakes
- Assuming a missing
__schemameans the API is safe - attackers can still use__typeto enumerate one type at a time. - Over-relying on deprecation warnings - deprecated fields are often left behind unintentionally and may still be reachable.
- Neglecting custom scalars - many bugs hide in JSON, Any, or Upload types that bypass validation.
- Failing to test depth-limited queries - some servers limit query depth; attackers can split the schema extraction across multiple shallow queries.
- Ignoring authorization on introspection itself - applying auth only to root queries leaves the schema exposed.
Real-World Impact
In 2022, a major fintech platform exposed its entire GraphQL schema to unauthenticated users. Attackers harvested the schema, discovered a deprecated transferFunds mutation that bypassed two-factor checks, and exfiltrated $1.2 M before the bug was patched. The incident underscores how a seemingly innocuous feature can become a high-value foothold.
From a defender’s perspective, the presence of a full schema in the wild is a red flag: it dramatically reduces the time needed for an attacker to craft a successful payload. Organizations should treat schema exposure as a critical asset that must be guarded, just like API keys or source code.
Looking forward, as GraphQL adoption expands, we expect more “schema-as-code” pipelines that automatically generate introspection files for CI. Attackers will likely target those artefacts in CI/CD leaks, making the need for strict access controls even more urgent.
Practice Exercises
- Exercise 1 - Manual Harvest: Use
curlto retrieve the full schema from a public GraphQL endpoint (e.g., countries.trevorblades.com). Identify allStringarguments in mutations. - Exercise 2 - Automated Dump: Install
gqlmapand run it against a deliberately vulnerable test server (e.g., graphqldemo). Export the schema toschema.jsonand generate a visual diagram with Voyager. - Exercise 3 - Find Deprecated Fields: Write a
jqfilter that lists every field markedisDeprecated: true. For each, craft a simple mutation that supplies a suspicious payload (e.g.,"'; SELECT 1;--"). - Exercise 4 - Enum Bypass: Locate an enum used for role selection. Attempt to set the role to the highest privilege using a mutation. Document the server’s response.
- Exercise 5 - Defense Implementation: In a local Apollo Server project, disable introspection for production, add a rate-limit middleware, and remove a deprecated field. Verify that the schema can no longer be retrieved without authentication.
These labs reinforce the complete workflow from discovery to exploitation and finally to remediation.
Further Reading
- “GraphQL Security Cheat Sheet” - OWASP
- “Apollo Server Security Best Practices” - Apollo Docs
- “The Dark Side of Introspection” - Black Hat 2023 Talk
- “gqlmap: GraphQL Recon & Exploitation Framework” - GitHub Repository
- “GraphQL Voyager - Visualising Schemas” - Official Documentation
Summary
Introspection is a double-edged sword: it empowers developers but also hands attackers a complete blueprint. By mastering the query syntax, automating extraction, and analysing the resulting type system for hidden fields, deprecated endpoints, and risky scalars, security professionals can both uncover critical vulnerabilities and advise on robust mitigations. Remember to treat the schema as a high-value asset, enforce strict access controls, and regularly audit for leftover deprecated resolvers.