安全测试-06-安全测试中的证据留存、复现脚本与提单写法

很多安全问题不是死在“没发现”,而是死在“发现了但推进不动”。

最常见的现场是这样的:

  • 测试说这里有越权
  • 研发看了一眼,说自己没复现出来
  • 产品问影响到底有多大
  • 最后来回拉扯两轮,这个问题就被拖虚了

所以安全测试里,真正决定问题能不能被快速修掉的,往往不是一句漏洞名词,而是三样东西:

  • 证据留存是否完整
  • 复现脚本是否稳定
  • 提单写法是否把影响、边界、复现、修复建议讲清楚

这篇文章就只讲这三件事。

一、为什么很多安全问题推进不动

我见过最常见的几种情况:

  • 只有截图,没有原始请求
  • 只有请求,没有最终业务结果
  • 只有一次偶发复现,没有稳定步骤
  • 只写“存在越权风险”,没写清楚谁能越谁、越过去后能做什么
  • 只写“建议加权限校验”,没说明应该在什么层加

这种输出方式有一个共同问题:

它更像提醒,不像可执行问题单。

而研发真正需要的是:

  • 问题在哪
  • 怎么稳定看到
  • 最终影响是什么
  • 代码应该往哪一层修

二、更适合的证据包结构:一单问题一份证据链

如果是安全问题,更倾向于把证据按“单个问题包”收,而不是散落在聊天记录里。

一个更完整的问题证据包,至少包括:

证据 作用
问题标题 快速说明是什么问题
业务场景 解释问题发生在什么动作里
测试账号/角色 说明是谁触发的
原始请求 说明请求真实长什么样
改造后请求 说明具体篡改了什么
响应结果 说明接口表现
最终业务结果 说明问题是否真实生效
截图/录屏 说明用户视角或后台结果
日志/审计记录 说明服务端实际落地情况

这里最关键的是:

接口回包不等于问题成立,最终业务结果才更接近结论。

例如一个越权问题,真正有力的证据通常不是“返回 200”,而是:

  • 普通用户真的看到了别人的数据
  • 普通用户真的把审批状态改掉了
  • 普通用户真的导出了超出权限范围的数据

三、复现脚本最重要的,不是长,而是稳定和最小化

很多问题单都会附一长串操作步骤,但仍然很难复现。
原因通常是步骤太散,依赖太多人工动作。

更适合的复现方式是:

1. 先把问题收敛成最小动作

例如一个越权审批问题,最小动作通常不是:

  1. 登录系统
  2. 打开菜单
  3. 找到审批页
  4. 点击若干按钮

而是:

  1. 准备一个待审批任务 taskId
  2. 使用普通用户身份
  3. 重放审批请求
  4. 验证状态是否变化

这样研发更快抓到问题核心。

2. 能脚本化就尽量脚本化

复现脚本不一定非要复杂,哪怕只是一条 curl 也很有价值。

例如:

1
2
3
4
curl 'https://example.test/api/approval/pass' \
-H 'Authorization: Bearer ordinary-user-token' \
-H 'Content-Type: application/json' \
--data-raw '{"taskId":"A20210422017","result":"pass"}'

如果再补一条结果校验脚本,价值会更高:

1
2
curl 'https://example.test/api/approval/detail?taskId=A20210422017' \
-H 'Authorization: Bearer ordinary-user-token'

这比一堆截图更容易让研发快速对上问题。

3. 脚本里只保留必要变量

一个更适合落库的问题脚本,通常只保留:

  • 环境地址
  • 测试账号/Token
  • 关键资源 ID
  • 关键请求体

不要把无关头、随机参数、浏览器噪音信息全塞进去,不然后续复现反而更难维护。

四、更常用的一套提单结构

如果是安全问题,更稳的做法通常是把缺陷单写成下面这几个固定部分。

1. 问题标题

标题要直接体现:

  • 问题类型
  • 问题对象
  • 影响动作

例如:

  • 普通用户可越权审批他人请假单
  • 已退出登录的旧会话仍可调用用户导出接口
  • 优惠券领取接口可被重复请求多次生效

比起“存在安全风险”,这种标题对研发更有效。

2. 业务场景

要写清楚问题在哪条链路里发生。

例如:

  • OA 审批系统,员工提交请假单后由主管审批
  • 运营后台支持按部门导出用户数据
  • 活动系统支持用户领取一次性优惠券

没有业务场景,研发和产品很难快速判断影响范围。

3. 复现步骤

步骤尽量短、准、可执行。

更倾向写成:

  1. 使用普通用户 user_a
  2. 准备待审批单 A20210422017
  3. 请求 /api/approval/pass
  4. Authorization 替换为普通用户 Token
  5. 观察返回和审批状态

4. 实际结果与预期结果

这是问题单里非常关键的一部分。

例如:

  • 实际结果:普通用户请求返回 200,审批状态变为“已通过”
  • 预期结果:普通用户应返回 403,审批状态不应变化

5. 影响说明

这里不要只写“有风险”,要写清楚:

  • 谁可以利用
  • 能做什么
  • 影响的是单资源还是批量资源
  • 影响范围是普通用户、运营、管理员还是跨租户

6. 证据附件

建议附:

  • 请求样例
  • 响应样例
  • 结果截图
  • 录屏
  • 日志摘录
  • 最小复现脚本

7. 修复建议

修复建议不要写成空话。

不要只写:

  • 建议修复权限问题

更有效的写法应该像这样:

  • 在审批接口服务端增加当前登录用户与任务处理人关系校验
  • 角色变化后主动失效旧 Session
  • 对领券动作增加幂等控制和唯一约束

五、证据留存最容易踩的几个坑

1. 只截前端页面,不保留原始请求

这样研发通常第一反应就是:是不是页面缓存问题、是不是前端展示问题。

2. 只保留请求,不保留最终结果

例如一个重放问题,单看接口回包可能不明显,但最终数据库状态、列表数据、审计日志已经变了。

3. 录屏太长,没有关键时间点

很多录屏从登录开始录 5 分钟,研发很难快速定位关键动作。

更好的做法是:

  • 保留短录屏
  • 标记关键时间点
  • 同时附请求样例

4. 复现依赖测试环境偶发状态

例如资源数据不固定、审批单状态会变、角色关系会被别人改。
这种问题如果不先准备稳定测试数据,复现很容易漂。

六、一套更实用的最小交付模板

如果要把安全问题沉淀成团队可复用模板,建议至少固定成这样:

1. 问题摘要

  • 标题
  • 环境
  • 问题类型
  • 严重级别

2. 业务背景

  • 模块
  • 触发条件
  • 相关角色

3. 复现资料

  • 最小复现步骤
  • 最小复现脚本
  • 关键请求
  • 关键参数

4. 结果证据

  • 响应结果
  • 页面结果
  • 日志/审计记录
  • 截图/录屏

5. 影响与修复建议

  • 实际影响
  • 边界说明
  • 建议修复点
  • 建议回归点

这套模板的价值是统一输出语言,减少团队来回解释。

七、真实案例:为什么“已经提了越权问题”,研发还是说复现不了

场景

一个后台系统存在导出越权问题。
测试最初提单时写的是:

  • 普通用户可以导出不属于自己的数据

并附了一张导出成功的截图。

执行

研发收到后,按截图里的页面路径自己操作了一遍,没有复现成功。
原因是:

  • 测试当时使用的是一个特殊账号
  • 导出数据范围依赖一个特定 deptId
  • 请求里还改过一次参数
  • 这些关键信息都没写进缺陷单

现象

最终出现的局面很典型:

  • 测试坚持说问题存在
  • 研发坚持说自己复现不了
  • 产品不清楚影响范围
  • 问题单在几次评论后变得越来越乱

排查

后面我重新把问题单整理了一次,只保留最关键的信息:

  • 账号角色:普通运营账号
  • 前置数据:准备一个不属于该账号部门的数据集
  • 原始请求:导出接口请求体
  • 改动点:将 deptId 替换为其他部门 ID
  • 结果校验:导出文件中真实包含了他部门数据

同时补了一条最小复现脚本和一张导出结果截图。

修复

重新整理后,研发很快定位到问题点:

  • 服务端只校验了“是否有导出权限”
  • 没校验“是否只能导出当前账号数据范围”

最后这单能快速推进,不是因为技术突然变简单了,而是因为输出终于变成了可执行问题单。

这个案例说明得很直接:

安全问题发现只是第一步,证据和复现方式决定它能不能真正落到修复。

八、怎么把这件事沉淀成团队标准

更合适的做法是直接把安全问题输出做成固定规范。

至少固定这几条:

  • 每单问题必须附最小复现步骤
  • 高风险问题必须附最小复现脚本
  • 必须同时附接口证据和业务结果证据
  • 标题必须包含问题类型、对象、动作
  • 修复建议必须指向服务端校验点或状态控制点

这样后面无论是谁发现问题,输出质量都会稳很多。

九、写在最后

安全测试里,发现问题当然重要,但真正让问题能被修掉、能被回归、能被团队吸收的,是后面的输出质量。

说到底,高质量的安全问题单应该同时满足三件事:

  • 研发能稳定复现
  • 产品能快速理解影响
  • 测试能明确知道以后怎么回归

如果这三件事都做到了,安全测试就不再只是“提出风险”,而是真正把风险往修复闭环推进了一步。