name: idor-detection description: IDOR 水平越权检测 — 通过替换资源标识符(ID/UUID/路径)访问他人资源的风险;适用于用户资料、订单、文档、租户隔离场景。 when-to-use: 当通过替换资源标识符(ID/UUID/路径)可能访问他人资源时 allowed-tools: bash,read_file,list_files,rg user-invocable: false
IDOR 水平越权检测
成因引用
IDOR 成因:source(用户可控的资源标识——path param / query / json body 字段 / header / cookie 中的 id/uuid/订单号/邀请码/任意"指向某条资源"的值)→ sink(资源访问决策:数据库查询不带 owner/tenant 过滤、文件路径不验所有权、API 按 ID 直接返回任意资源、对象引用不校验归属)。判定按 sink 语义"是否将当前请求主体与目标资源归属绑定校验"——业务命名(订单号、工单号、邀请码、UUID)只是表层。详见同根目录 pentest/web-security-testing/SKILL.md 漏洞成因图谱 · IDOR 行(不在本 skill 重复成因)。
触发线索(基线检查项)
以下是已知的常见 IDOR 触发线索,作为基线起点而非必检硬清单。结合目标代码与上下文动态调整:
- 适用且已完成 → 标注
[x] done - 明确不适用 → 标注
[-] n/a (原因),原因要具体到代码事实 - 基线未列出但实际发现 → 新增条目并标注
[+] added (来源)
基线触发线索按"sink 语义"分类(不按业务命名):
- 资源详情查询:
/api/<resource>/{id}//api/<resource>?id=形态,包括 user / profile / order / ticket / message / document / contract - 附件 / 文件下载:
/files/{id}//attachments/{name}/?file_id=按 ID 直发文件流 - 批量 ID 列表:
?ids=1,2,3/ body{"ids":[...]}批量拉取(往往逐项归属校验缺失) - 导出 / 报表接口:
/export?order_id=//report?contract_id=按 ID 出导出文件 - 用户级嵌套资源:
/users/{uid}/orders/{oid}(外层 uid 是否真和当前主体一致需校验) - 间接对象引用:订单号、邀请码、分享 token、短链 ID——表面"猜不到"但泄露后即可访问
- 跨租户接口:含
tenant_id/org_id的查询接口,归属边界不只看 owner 还看租户 - 代码模式:后端
db.Where("id = ?", id).First(&res)等不带 owner / tenant 条件的查询;按 path 直接os.Open(...)不校验归属
思考检查点
加载本 skill 时按这些问题思考:
- 这个资源标识谁可控?后端在哪一步把它和当前主体的归属对齐?
- 同一端点是否还有其他归属维度(owner_id / tenant_id / org_id)需要同时校验?
- ID 类型是 UUID / 雪花 ID 还是顺序整数?UUID 不可枚举不等于无 IDOR,要看泄露路径
- 同子系统其他资源类型(订单 / 工单 / 附件 / 消息)的 owner 校验逻辑是否独立实现、是否一致?
- 写操作命中时,是否为不可逆动作?需走哨兵自证或非破坏差分
前置条件与安全边界
- 准备两组不同归属资源:
A 自有资源与B 自有资源。 - 仅在授权环境测试,不对真实业务数据做破坏性修改。涉及删除/覆盖/不可逆写时,按
common/closure-verification.md的《破坏性 / 不可逆动作的闭环边界》执行——优先哨兵自证或非破坏差分,二者都做不到就停suspected,不得对真实业务数据执行破坏动作来强行闭环。 - 单资源类型默认最多 8 次请求(A 基线、B 对照、复验)。
- 单轮只替换一个资源标识,避免多变量干扰。
检测步骤
Step 1:基线与归属字段识别
- 识别资源标识符位置(路径、查询参数、请求体、批量 ID 列表、header、cookie)
- 账号 A 访问 A 资源建立基线,记录关键归属字段(owner_id、tenant_id、user_id)
- 从端点账本(
recon-methodology产出的endpoint-ledger.jsonl)查询所有"按 ID 取资源"的端点,按 sink 语义而非业务名筛选
Step 2:向量①替换
账号 A 凭证不变,仅将资源标识替换为 B 资源并重放,观察是否能读取/操作 B 资源。验证响应中归属字段确为 B 主体(不是 A 自己的 / 不是公开样本)。
Step 3:向量②置空 / fail-open
将资源标识或归属参数置空、置 0、置 null、或整条删除(?id=、?user_id=0、删除 body 字段),探测后端是否 fail-open——空归属退化为「查全表 / 默认放行」从而泄露他人数据或绕过归属校验。
Step 4:向量③参数污染 / 跨位置
- 重复提交归属参数(
user_id=A&user_id=B、query 与 body 同名冲突、JSON 重复键),探测后端取首/取尾/合并的差异 - 同一资源经不同参数位(path vs body/query)、不同 API 版本(
/v1vs/v2)访问,鉴权严格度可能不一致 - 对写操作执行回读验证;若为删除/覆盖/不可逆写,按《破坏性 / 不可逆动作的闭环边界》改用 B 名下哨兵资源或非破坏差分
次级向量(主向量无果时补测):批量接口的"逐项归属"检查是否生效(
ids=自己的,别人的是否混入他人数据);间接对象引用(订单号 / 邀请码)枚举或泄露后的可访问性。
示例库
正例形态(代码层根因)
db.Where("id = ?", c.Param("id")).First(&order)— 查询不带 owner_id 过滤(path-id-query-no-owner-filter-idor)c.File(filepath.Join(attachRoot, c.Query("name")))— 按文件名直发,未校验归属(attachment-filename-direct-serve-idor)db.Find(&users, c.QueryArray("ids"))— 批量接口逐项归属未校验(batch-ids-no-owner-filter-idor)db.Where("uuid = ?", uuid).First(&doc)— UUID 不可枚举但泄露后无归属校验(uuid-leak-no-owner-check-idor)
窄化反例(必须避免)
以下是 IDOR 维度的典型窄化误判:
- "ID 是 UUID → 不算 IDOR" — 错。UUID 不可枚举不等于无 IDOR,泄露路径很多(日志 / Referer / 邀请链接 / 分享 token / 前端泄露);按 sink 语义"是否校验所有权"判定,不按 ID 是否可枚举判定
- "已测了
/api/users/{id}→ 跳过其他资源类型" — 错。每种资源类型(订单 / 工单 / 附件 / 消息 / 报表 / 合同 / ...)的 owner 校验逻辑独立实现,需独立测,不能用一个端点结论代表全站 - "前端有权限控制 / 不显示该资源 → 后端必然有归属校验" — 错。前端控制可绕过(直连 API),后端必须独立验证
- "已在子系统 A 命中 IDOR → 推断子系统 B 也是" — 错。跨子系统独立结账,不同子系统可能由不同团队 / 不同时期开发,命中率不可推广
- "状态码 200 + 返回数据 → 必然是 IDOR" — 不严谨。需校验归属字段确为他人资源,排除返回的是自己的资源 / 公开样本 / 脱敏空壳
反例义务(必须遵守)
为什么这里是「必须」:反例义务属于交付契约——"未发现 IDOR"或"已防护"结论是覆盖完整性的产物声明,缺失反向验证清单会让下游误信"该维度全站安全"。
写"未发现 IDOR"或"归属校验已生效"前,产物必须包含:
- 测过的"按 ID 取资源"端点完整清单(按 sink 语义枚举:所有资源详情 / 附件下载 / 批量 ID / 导出 / 嵌套资源 / 间接对象引用形态,不按业务名或参数名筛选)
- 每个端点尝试的 payload 形态(替换、置空、参数污染、跨位置 / 跨版本、批量混入)
- 每个端点的对照证据(A 基线响应、A 用 B 标识的响应、归属字段值)
清单不完整 → 结论降级为 partial-coverage 并显式声明未覆盖范围(例如:仅测了用户资料类、未测附件 / 工单 / 报表)。
闭环验证要求(必须遵守)
通用闭环口径见同根目录 common/closure-verification.md(技能表 path 列同一抽取根下,需要时 read_file 读取)。核心:结论须形成「输入 → 处理 → 真实危害 → 可复核证据」完整证据链;仅凭状态码、响应差异等中间信号最多判 suspected,证明跨主体真实读取/写入才判 confirmed。
实际效果验证方向(至少证明一类)
- 跨主体读取:A 读取到 B 的真实资源内容、字段或文件,归属字段值(owner_id / tenant_id)经确认为 B
- 跨主体修改:A 对 B 的资源执行写操作,并通过回读/状态变化证明影响真实生效。若该写为不可逆动作(删除/覆盖/批量改),改用 B 名下的哨兵资源或非破坏差分自证,不得对 B 的真实资源执行(见《破坏性 / 不可逆动作的闭环边界》)
- 若只有 200、空壳响应或 ID 可猜测,但未证明跨主体真实访问效果,不能给
confirmed
判定标准
| 现象 | 判定 |
|---|---|
| 账号 A 成功读取或修改账号 B 资源,并通过真实回读或状态变化证明跨主体影响成立 | confirmed |
| 行为异常(如 200 空壳)但尚未证明跨主体真实读取或写入效果 | suspected |
| 账号 A 无法访问 B 资源(403/404 且无泄露) | not vulnerable |
| 只测了部分资源类型(如仅用户资料),未覆盖附件 / 工单 / 批量 / 间接对象引用 | partial-coverage(不得宣称 safe) |
不可逆动作例外:上表 confirmed 行涉及对 B 资源删除/覆盖/不可逆写时,按
common/closure-verification.md《破坏性 / 不可逆动作的闭环边界》——不得对 B 的真实资源执行,改用 B 名下哨兵资源或非破坏差分,二者都做不到则降suspected。
修复建议
- 资源查询同时绑定"资源 ID + 当前主体 ID/租户 ID",归属校验下沉到数据访问层而非控制器
- 禁止仅依赖前端或客户端传入的
user_id/tenant_id决策权限,归属字段从服务端 session/token 取 - 对批量接口逐项做归属校验,不因单项合法而整体放行;返回前对结果集做归属过滤
- 间接对象引用(订单号 / 邀请码 / 分享 token)也走归属或时效 / 一次性 token 校验
- 为关键资源接口补充 IDOR 回归测试(跨用户/跨租户场景)