本文面向获得书面授权的渗透测试人员、红蓝对抗团队及 GraphQL 服务开发运维人员,验证重点是:同一查询在不同角色下,是否返回了超出该角色授权范围的数据。测试仅应在隔离环境或明确批准的目标与时间窗口内进行;使用专用测试账号和合成数据,预先约定请求速率、停止条件、日志保留方式及数据清理责任。不得将以下示例直接用于未授权目标,也不应通过批量枚举或高并发请求扩大验证范围。

一、明确范围与安全控制
开始前记录授权文件或工单编号、目标环境、GraphQL 接口地址、测试账号及角色、允许的测试时间、请求速率上限和紧急联系人。优先使用隔离测试环境;若只能在生产环境验证,应限定为专用测试对象,并得到系统负责人明确批准。
建议在测试计划中预先写明以下停止条件:
- 响应延迟、错误率或业务告警明显异常;
- 请求触及真实用户或非测试对象的数据;
- 出现可能造成写入、删除、通知发送或外部副作用的字段或变更操作;
- 无法确认某个 ID 对应的是专用测试数据;
- 目标、账号或时间窗口超出授权范围。
测试期间采用低频、单请求验证,不进行批量 ID 遍历、并发压测或大规模字段展开。GraphQL 查询可能在一次 HTTP 请求中组合多个字段,因此请求数量少并不必然代表服务端负载低。
二、梳理攻击面:从文档与 Schema 建立假设
先检查授权范围内公开提供的 API 文档、客户端代码、测试环境说明和已批准的接口清单。记录可见的查询入口、对象类型、关联关系、敏感字段以及身份认证方式。文档只是线索,不能据此认定实际权限行为。
如果授权允许,可以在测试环境中进行范围受限的 Schema 内省,确认哪些类型和字段可见。以下示例只请求根查询类型名称,不枚举整个 Schema:
query InspectRoot {
__schema {
queryType {
name
}
}
}
用占位地址发送请求时,可先把请求体保存为本地文件,并避免把访问令牌写入脚本、命令历史或报告:
export GQL_URL='https://<authorized-host>/graphql'
curl -sS -X POST "$GQL_URL"
-H 'Content-Type: application/json'
-H 'Authorization: Bearer <TEST_TOKEN>'
--data-binary @request.json
curl 适合复现并保存单个请求;代理工具适合在授权范围内检查和重放请求;GraphQL IDE 则可能自动发送内省或辅助查询。不同工具对认证头、变量、代理设置、自动补全和请求历史的处理并不相同。执行前应确认当前工具不会自动生成高复杂度查询,也不要启用未经批准的自动扫描或批量重放功能。
内省被关闭不等于不存在授权问题;内省开放也不必然意味着漏洞。判断重点应是:外部可见的 Schema 是否暴露了不应公开的业务信息,以及实际解析器是否对每次请求执行正确的授权校验。Schema 隐藏不能替代服务端权限控制。
三、建立角色与对象级授权假设
在测试矩阵中列出角色、测试对象及预期访问结果。例如,准备两个专用账号:账号 A 创建或拥有测试对象,账号 B 不拥有该对象。确认对象 ID 来自测试数据,不使用猜测、枚举或真实用户数据。
| 角色 | 测试对象 | 预期结果 |
|---|---|---|
| 账号 A | A 所拥有的测试对象 | 仅返回 A 有权查看的字段 |
| 账号 B | A 所拥有的测试对象 | 拒绝访问,或按设计返回不泄露信息的结果 |
| 账号 B | B 所拥有的测试对象 | 仅返回 B 有权查看的字段 |
随后提出可验证的假设:
- 对象级授权:更换为另一测试对象的 ID 后,服务端是否仍依据当前用户身份检查对象归属?
- 字段级权限:低权限角色是否能通过直接请求读取仅限高权限角色访问的字段?
- 嵌套关系:父对象可访问时,其关联对象或敏感字段是否也经过独立授权?
- Schema 内省:公开内容是否与服务设计一致,是否意外暴露内部类型、管理字段或敏感业务命名?
- 查询深度限制:超过预期嵌套层级的合法查询是否被限制,限制行为是否稳定且可观测?
GraphQL 中字段出现在 Schema 里,只能说明它可能可被请求,不能说明每个角色都应有权读取。反过来,界面没有展示某个字段,也不能证明服务端禁止访问。
四、用同一查询进行最小化验证
优先选择只读查询和合成数据。先以账号 A 请求其拥有的对象,确认查询本身有效;再保持查询结构不变,仅替换为账号 B 的认证信息。随后,在授权明确且对象为测试数据的前提下,使用账号 B 请求 A 的测试对象。除身份与测试对象外,尽量不改动其他条件,以便将结果差异归因于授权边界。
示例需按目标 Schema 调整,字段名仅作占位:
query ReadTestObject($id: ID!) {
testObject(id: $id) {
id
ownerId
displayName
}
}
请求体示例:
{
"query": "query ReadTestObject($id: ID!) { testObject(id: $id) { id ownerId displayName } }",
"variables": {
"id": "<TEST_OBJECT_ID>"
}
}
每个角色各自使用对应的测试令牌发送同一请求。观察响应中的数据、错误、空值和 HTTP 状态,但不要仅凭“返回 200”或“出现错误消息”判断授权成功或失败:GraphQL 服务可能在 HTTP 成功状态下返回字段级错误,也可能为避免泄露对象是否存在而统一返回空值或通用拒绝结果。应结合预期访问规则、服务端日志以及测试账号的数据归属进行判断。
字段级权限验证也应沿用同样原则:选择合成对象及已明确标记为敏感的测试字段,先以有权角色验证字段确实可读,再以无权角色发送相同查询。记录该字段是被拒绝、被省略、返回空值,还是意外返回数据。不要为了“证明更多”而扩大字段列表;只验证批准的字段和对象。

五、检查查询深度限制而不制造负载
查询深度限制用于约束嵌套查询的复杂程度,但具体行为取决于服务实现和业务 Schema。不要通过大量别名、重复分支、深层循环关系或巨型查询制造压力。应从一条已知有效、低成本的只读查询开始,逐步增加少量、业务上合理的嵌套层级,每次只改变一个条件,并按预先约定的低频率发送。
例如,可在测试 Schema 确实支持的前提下,逐步检查对象与关联对象的有限层级:
query CheckNestedAccess($id: ID!) {
testObject(id: $id) {
id
relatedItem {
id
}
}
}
记录服务对不同层级的校验结果、错误信息及耗时趋势。若响应时间明显增长、出现告警或触发停止条件,立即停止,不再增加深度。一次低频验证只能说明该环境下观察到的限制行为,不能替代经批准的容量或抗压测试。
六、保存最小、可复核的证据
证据应足以让开发团队复现判断,但不应包含不必要的个人信息、真实业务数据或长期有效的凭据。建议为每次验证记录:
- 授权编号、环境、时间和测试人员;
- 接口路径、请求操作名称、角色及测试对象标识;
- 预期行为与实际响应差异;
- 请求和响应的必要片段,已移除令牌、Cookie、个人信息及非测试数据;
- 相关服务端日志或关联 ID(如获准访问);
- 测试步骤、工具名称及版本、代理或认证设置等必要环境信息;
- 影响范围、停止条件是否触发,以及是否执行了数据清理。
保存证据时使用受控存储并遵守约定的保留期限。不要把完整响应直接贴入工单或聊天系统;只保留能说明授权边界问题的最小片段。
七、判断风险并给出整改方向
风险判断应结合数据敏感性、可访问角色、对象范围、利用前提和实际业务影响,不应仅按查询是否成功定级。低权限用户能读取其他用户的敏感对象,通常比查询到公开资料或通用类型名称影响更高;若只观察到 Schema 信息暴露而未验证实际数据访问,应将结论限定为信息暴露,不要推断为对象越权。
整改建议应落到服务端授权逻辑:
- 在解析器或统一服务层依据当前身份、对象归属和业务策略执行对象级校验;不能依赖客户端隐藏字段或不可预测的 ID。
- 对敏感字段单独执行字段级权限检查,并覆盖嵌套对象、关联解析器和不同查询入口。
- 遵循默认拒绝原则;对拒绝、空值和错误响应的设计进行一致性检查,避免泄露不必要的对象存在信息。
- 按业务需求评估是否限制内省及其暴露范围,但不要把关闭内省当作授权修复。
- 配置并验证查询深度、复杂度或资源预算等限制,同时结合超时、速率控制和监测;具体阈值应在目标服务的受控测试中确定。
- 对异常拒绝、过深查询和高成本请求保留必要的监测信号,并避免在日志中记录令牌或敏感响应正文。
八、复测与清理
修复后,用同一组测试账号、测试对象和查询重新验证,确保有权角色仍可完成预期操作,无权角色不能读取对象或敏感字段。对查询深度限制,应确认预期查询仍可用、超出策略的请求被安全拒绝,并且常规业务没有因限制产生不必要的失败。
结束时撤销临时令牌或测试账号权限,删除测试期间创建的合成数据,清除本地请求文件、代理历史和临时日志副本;按授权约定保留必要证据。若发现真实数据暴露、业务异常或测试影响超出预期,应停止操作并立即通知系统负责人。
一份可复核的结论应明确写出:测试范围与授权条件、验证角色和对象、同一查询的角色差异、证据及其脱敏方式、影响判断、整改建议、清理情况和复测结果。这样才能把一次 GraphQL 渗透测试转化为可验证、可整改且不会扩大业务风险的安全闭环。

上海市南汇区 1F
终于有篇讲对象级授权的教程了