GraphQL Introspection in Production Is a Gift to Attackers
Leaving GraphQL introspection enabled in production hands your entire API schema to anyone with curl. I've seen this in the wild and it's worse than you think.
Introspection is one of GraphQL's best developer experience features and one of the most consistently mishandled security decisions I see in production APIs. Leaving it on is the equivalent of publishing your entire database schema on your homepage — every type, every field, every relationship, every mutation, fully documented and machine-readable.
I've audited a handful of client APIs over the years, and more than once I've pulled a full schema from a production GraphQL endpoint in under two minutes using nothing but a browser and a public introspection query. No auth token. No credentials. Just a POST request and a wall of JSON describing exactly how their entire backend is shaped.
What Introspection Actually Does
GraphQL introspection is a built-in query mechanism that lets clients ask a server "what types do you have?" and "what can I do here?" It's how GraphiQL, Insomnia, Postman, and every other GraphQL tool builds its autocomplete and documentation UI. In development, it's invaluable. You get a self-documenting API out of the box.
The problem isn't the feature. The problem is that every major GraphQL library ships with introspection enabled by default and most teams never touch that setting when they go to production.
Here's the introspection query that exposes everything:
{
__schema {
types {
name
fields {
name
type {
name
kind
}
}
}
}
}
Run that against a production endpoint and you get back a complete map of every queryable type, every mutation, every input object, every enum. Tools like InQL will take that output and automatically generate attack payloads for Burp Suite. This isn't theoretical — it's a documented pentesting workflow.
The Real-World Risk
I integrated a GraphQL API for a healthcare-adjacent client a couple of years ago. During a pre-launch security review, I ran an introspection query against their staging endpoint — which was, it turned out, pointed at production data behind a thin staging URL. The schema came back instantly. It exposed internal type names that made it obvious which EHR system they were using, which third-party billing platform, and the shape of their patient record objects — including fields like insuranceGroupNumber, diagnosisCode, and authorizationStatus.
None of that data was actually accessible without auth tokens. But the schema alone told an attacker exactly what to probe for, what mutation names to try, and what the internal architecture looked like. In security terms, you've just handed over your reconnaissance for free.
Field names are not neutral. deletePatientRecord, adminOverrideApproval, internalBillingAdjustment — these are breadcrumbs. A motivated attacker uses introspection to map your entire attack surface before writing a single exploit.
Disabling It in Laravel with Lighthouse
If you're running GraphQL in Laravel, Lighthouse is the de facto standard. Disabling introspection in production is a one-liner in config/lighthouse.php:
// config/lighthouse.php
'security' => [
'max_query_complexity' => 200,
'max_query_depth' => 15,
'disable_introspection' => \Nuwave\Lighthouse\Security\DisableIntrospection::class,
],
But I don't just flip that switch globally. I conditionalize it on environment so I don't destroy my own dev experience:
// config/lighthouse.php
'security' => [
'max_query_complexity' => 200,
'max_query_depth' => 15,
'disable_introspection' => env('APP_ENV') === 'production'
? \Nuwave\Lighthouse\Security\DisableIntrospection::class
: \Nuwave\Lighthouse\Security\AlwaysAllowIntrospection::class,
],
This way, local and staging environments keep full introspection, and production silently returns an error on introspection queries rather than your whole schema.
If you're using a raw webonyx/graphql-php setup without Lighthouse, you control this at the execution layer:
use GraphQL\GraphQL;
use GraphQL\Validator\Rules\DisableIntrospection;
use GraphQL\Validator\DocumentValidator;
if (app()->environment('production')) {
DocumentValidator::addRule(new DisableIntrospection());
}
$result = GraphQL::executeQuery(
$schema,
$query,
null,
null,
$variables
);
Two approaches, same outcome: introspection queries return an error in production instead of your schema.
The Gotchas
Apollo Studio and similar tools break. If you're using Apollo's cloud tooling, schema registry, or operation checks, those rely on introspection or an uploaded schema. Disabling introspection means you need to push your schema to Apollo explicitly via rover schema push rather than letting it introspect. Not hard, but it surprises teams when their Studio dashboard goes blank after a production deploy.
Your internal tooling might rely on it. I've inherited codebases where an internal admin tool was dynamically building its UI by introspecting the production API at runtime. Fragile architecture, yes, but it existed. Before you disable introspection, audit whether anything in your own stack is calling __schema or __type queries in production.
Disabling introspection is not a substitute for authorization. I want to be clear about this. If your mutations don't check permissions, hiding the schema doesn't save you. An attacker can still guess field names, try common patterns, or pull your JavaScript bundle and find the query strings there. Introspection is reconnaissance. You still have to lock down the actual operations. Disabling introspection raises the cost of reconnaissance — it doesn't fix bad auth.
Partial introspection is a middle ground some teams use. Some libraries let you selectively block introspection on specific types or fields while allowing it elsewhere. In practice I've never found this worth the complexity. Either your schema is sensitive or it isn't. If it is, turn the whole thing off in production.
When I'd Leave It On
Public APIs where the schema is intentionally public. GitHub's GraphQL API, Shopify's Storefront API — these publish their schemas in docs anyway. Introspection isn't leaking anything there. It's just convenient.
Internal tools behind a VPN or hard auth boundary where the audience is your own developers and the schema isn't sensitive. If it's employees querying an internal data warehouse through a GraphQL layer, and they already have credentials to get there, hiding the schema from them is just friction.
When I'd Turn It Off
Anything customer-facing. Any API that touches PII, financial data, health data, or proprietary business logic. Any endpoint reachable from the public internet, even if it requires authentication, because you're reducing attack surface and that's always worth doing.
The rule I use: if you wouldn't publish your schema in a public GitHub repo, don't leave introspection on in production.
One More Thing: Query Depth and Complexity
While you're in lighthouse.php tightening up introspection, also set max_query_depth and max_query_complexity. GraphQL is vulnerable to denial-of-service through nested queries — the classic "10,000 nested friends" attack. These limits cost you nothing in normal operation and stop a whole category of abuse. I set depth to 10-15 and complexity to 200-400 depending on the schema, then tune from there based on what legitimate client queries actually look like.
It's the same principle: GraphQL's power is also its attack surface. Introspection, unbounded depth, unbounded complexity — all three are on by default in most libraries, all three should be explicitly configured before you ship.
GraphQL introspection being enabled by default made sense in 2015 when the ecosystem was figuring itself out. It does not make sense now. Turn it off in production, conditionalize it on your environment, and move on. It takes ten minutes and it's one less thing on your threat model.
Need help shipping something like this? Get in touch.