安全测试-06-安全测试中的证据留存、复现脚本与提单写法
很多安全问题不是死在“没发现”,而是死在“发现了但推进不动”。
最常见的现场是这样的:
- 测试说这里有越权
- 研发看了一眼,说自己没复现出来
- 产品问影响到底有多大
- 最后来回拉扯两轮,这个问题就被拖虚了
所以安全测试里,真正决定问题能不能被快速修掉的,往往不是一句漏洞名词,而是三样东西:
- 证据留存是否完整
- 复现脚本是否稳定
- 提单写法是否把影响、边界、复现、修复建议讲清楚
这篇文章就只讲这三件事。
一、为什么很多安全问题推进不动
我见过最常见的几种情况:
- 只有截图,没有原始请求
- 只有请求,没有最终业务结果
- 只有一次偶发复现,没有稳定步骤
- 只写“存在越权风险”,没写清楚谁能越谁、越过去后能做什么
- 只写“建议加权限校验”,没说明应该在什么层加
这种输出方式有一个共同问题:
它更像提醒,不像可执行问题单。
而研发真正需要的是:
- 问题在哪
- 怎么稳定看到
- 最终影响是什么
- 代码应该往哪一层修
二、更适合的证据包结构:一单问题一份证据链
如果是安全问题,更倾向于把证据按“单个问题包”收,而不是散落在聊天记录里。
一个更完整的问题证据包,至少包括:
| 证据 | 作用 |
|---|---|
| 问题标题 | 快速说明是什么问题 |
| 业务场景 | 解释问题发生在什么动作里 |
| 测试账号/角色 | 说明是谁触发的 |
| 原始请求 | 说明请求真实长什么样 |
| 改造后请求 | 说明具体篡改了什么 |
| 响应结果 | 说明接口表现 |
| 最终业务结果 | 说明问题是否真实生效 |
| 截图/录屏 | 说明用户视角或后台结果 |
| 日志/审计记录 | 说明服务端实际落地情况 |
这里最关键的是:
接口回包不等于问题成立,最终业务结果才更接近结论。
例如一个越权问题,真正有力的证据通常不是“返回 200”,而是:
- 普通用户真的看到了别人的数据
- 普通用户真的把审批状态改掉了
- 普通用户真的导出了超出权限范围的数据
三、复现脚本最重要的,不是长,而是稳定和最小化
很多问题单都会附一长串操作步骤,但仍然很难复现。
原因通常是步骤太散,依赖太多人工动作。
更适合的复现方式是:
1. 先把问题收敛成最小动作
例如一个越权审批问题,最小动作通常不是:
- 登录系统
- 打开菜单
- 找到审批页
- 点击若干按钮
而是:
- 准备一个待审批任务
taskId - 使用普通用户身份
- 重放审批请求
- 验证状态是否变化
这样研发更快抓到问题核心。
2. 能脚本化就尽量脚本化
复现脚本不一定非要复杂,哪怕只是一条 curl 也很有价值。
例如:
1 | curl 'https://example.test/api/approval/pass' \ |
如果再补一条结果校验脚本,价值会更高:
1 | curl 'https://example.test/api/approval/detail?taskId=A20210422017' \ |
这比一堆截图更容易让研发快速对上问题。
3. 脚本里只保留必要变量
一个更适合落库的问题脚本,通常只保留:
- 环境地址
- 测试账号/Token
- 关键资源 ID
- 关键请求体
不要把无关头、随机参数、浏览器噪音信息全塞进去,不然后续复现反而更难维护。
四、更常用的一套提单结构
如果是安全问题,更稳的做法通常是把缺陷单写成下面这几个固定部分。
1. 问题标题
标题要直接体现:
- 问题类型
- 问题对象
- 影响动作
例如:
- 普通用户可越权审批他人请假单
- 已退出登录的旧会话仍可调用用户导出接口
- 优惠券领取接口可被重复请求多次生效
比起“存在安全风险”,这种标题对研发更有效。
2. 业务场景
要写清楚问题在哪条链路里发生。
例如:
- OA 审批系统,员工提交请假单后由主管审批
- 运营后台支持按部门导出用户数据
- 活动系统支持用户领取一次性优惠券
没有业务场景,研发和产品很难快速判断影响范围。
3. 复现步骤
步骤尽量短、准、可执行。
更倾向写成:
- 使用普通用户
user_a - 准备待审批单
A20210422017 - 请求
/api/approval/pass - 将
Authorization替换为普通用户 Token - 观察返回和审批状态
4. 实际结果与预期结果
这是问题单里非常关键的一部分。
例如:
- 实际结果:普通用户请求返回
200,审批状态变为“已通过” - 预期结果:普通用户应返回
403,审批状态不应变化
5. 影响说明
这里不要只写“有风险”,要写清楚:
- 谁可以利用
- 能做什么
- 影响的是单资源还是批量资源
- 影响范围是普通用户、运营、管理员还是跨租户
6. 证据附件
建议附:
- 请求样例
- 响应样例
- 结果截图
- 录屏
- 日志摘录
- 最小复现脚本
7. 修复建议
修复建议不要写成空话。
不要只写:
- 建议修复权限问题
更有效的写法应该像这样:
- 在审批接口服务端增加当前登录用户与任务处理人关系校验
- 角色变化后主动失效旧 Session
- 对领券动作增加幂等控制和唯一约束
五、证据留存最容易踩的几个坑
1. 只截前端页面,不保留原始请求
这样研发通常第一反应就是:是不是页面缓存问题、是不是前端展示问题。
2. 只保留请求,不保留最终结果
例如一个重放问题,单看接口回包可能不明显,但最终数据库状态、列表数据、审计日志已经变了。
3. 录屏太长,没有关键时间点
很多录屏从登录开始录 5 分钟,研发很难快速定位关键动作。
更好的做法是:
- 保留短录屏
- 标记关键时间点
- 同时附请求样例
4. 复现依赖测试环境偶发状态
例如资源数据不固定、审批单状态会变、角色关系会被别人改。
这种问题如果不先准备稳定测试数据,复现很容易漂。
六、一套更实用的最小交付模板
如果要把安全问题沉淀成团队可复用模板,建议至少固定成这样:
1. 问题摘要
- 标题
- 环境
- 问题类型
- 严重级别
2. 业务背景
- 模块
- 触发条件
- 相关角色
3. 复现资料
- 最小复现步骤
- 最小复现脚本
- 关键请求
- 关键参数
4. 结果证据
- 响应结果
- 页面结果
- 日志/审计记录
- 截图/录屏
5. 影响与修复建议
- 实际影响
- 边界说明
- 建议修复点
- 建议回归点
这套模板的价值是统一输出语言,减少团队来回解释。
七、真实案例:为什么“已经提了越权问题”,研发还是说复现不了
场景
一个后台系统存在导出越权问题。
测试最初提单时写的是:
- 普通用户可以导出不属于自己的数据
并附了一张导出成功的截图。
执行
研发收到后,按截图里的页面路径自己操作了一遍,没有复现成功。
原因是:
- 测试当时使用的是一个特殊账号
- 导出数据范围依赖一个特定
deptId - 请求里还改过一次参数
- 这些关键信息都没写进缺陷单
现象
最终出现的局面很典型:
- 测试坚持说问题存在
- 研发坚持说自己复现不了
- 产品不清楚影响范围
- 问题单在几次评论后变得越来越乱
排查
后面我重新把问题单整理了一次,只保留最关键的信息:
- 账号角色:普通运营账号
- 前置数据:准备一个不属于该账号部门的数据集
- 原始请求:导出接口请求体
- 改动点:将
deptId替换为其他部门 ID - 结果校验:导出文件中真实包含了他部门数据
同时补了一条最小复现脚本和一张导出结果截图。
修复
重新整理后,研发很快定位到问题点:
- 服务端只校验了“是否有导出权限”
- 没校验“是否只能导出当前账号数据范围”
最后这单能快速推进,不是因为技术突然变简单了,而是因为输出终于变成了可执行问题单。
这个案例说明得很直接:
安全问题发现只是第一步,证据和复现方式决定它能不能真正落到修复。
八、怎么把这件事沉淀成团队标准
更合适的做法是直接把安全问题输出做成固定规范。
至少固定这几条:
- 每单问题必须附最小复现步骤
- 高风险问题必须附最小复现脚本
- 必须同时附接口证据和业务结果证据
- 标题必须包含问题类型、对象、动作
- 修复建议必须指向服务端校验点或状态控制点
这样后面无论是谁发现问题,输出质量都会稳很多。
九、写在最后
安全测试里,发现问题当然重要,但真正让问题能被修掉、能被回归、能被团队吸收的,是后面的输出质量。
说到底,高质量的安全问题单应该同时满足三件事:
- 研发能稳定复现
- 产品能快速理解影响
- 测试能明确知道以后怎么回归
如果这三件事都做到了,安全测试就不再只是“提出风险”,而是真正把风险往修复闭环推进了一步。