本文记录一次经授权的第三方软件测试项目全过程。测试对象为某垂直行业 SaaS 平台,涵盖功能、安全、性能、UX 体验四个维度,全程以 Claude Code 为 AI 协作核心,Python + Playwright 作为自动化测试引擎。客户名称、域名、账号、令牌及所有可识别信息均已脱敏,示例仅用于说明方法。
先说结论,再讲过程
14 个工作日,发现 31 项缺陷(严重 9 / 高 7 / 中 6 / 低 9),交付 3 份阶段性报告 + 1 份复测反馈,写了 28 个 Python 测试脚本,给出"暂不建议上线"的明确判断。
如果是传统测试方式------手工操作 + 手写报告 + 截图存档------完成同等覆盖面大约需要 6~8 周,且证据复现性远不如脚本。
这篇文章想回答一个问题:AI 工具,到底把测试的哪些环节改变了?
一、项目背景:三方委托的软件交付验收
这是一个典型的三方格局:甲方(业主)委托乙方(外包开发团队)开发了一套业务平台,临上线前请第三方对软件质量做独立检测。我的角色是第三方,任务是:
- 独立发现问题,不受开发方影响
- 出具可供法律存档的书面报告
- 对整改结果做复测验收,给出最终上线建议
被测系统是一套前后端分离的 SaaS 平台,包含 用户端(PC/H5)、后台管理端、门店端 三个子系统,后端 PHP ThinkPHP 框架,前端 Vue SSR 渲染,阿里云部署。
测试覆盖四个维度:功能测试、安全测试、性能压测、UX 体验评估。
二、开局:两天内发现三个"上线即爆炸"级问题
检测第一天,按功能走查 + 抓包节奏推进。Claude Code 帮我快速生成了侦查脚本,抓取站点结构、捕获 API 流量,比手工打开 Devtools 慢慢翻效率高一个数量级:
# 侦查脚本核心:监听所有 /api/ 请求,记录 URL、方法、鉴权头、Body 摘要
calls = []
def on_req(req):
if "/api/" in req.url:
body = req.post_data or ""
auth = {k: v for k, v in req.headers.items()
if k.lower() in ("user-token", "authorization", "sign")}
calls.append({"url": req.url, "method": req.method,
"auth": auth, "body": body[:200]})
page.on("request", on_req)
page.goto(BASE_URL + "/user/orders/mall", wait_until="networkidle")
脚本跑完,150+ 条接口路径打出来,一眼就看到了几个高风险模式。
发现 #1:支付是假的
下单流程走了一遍,不到1秒订单状态变为"支付成功 / 待发货"。没有跳微信支付,没有跳支付宝------完全自动置状态。
翻后台配置:微信商户号空、API密钥空、支付宝签约空。整个支付链路是个空壳,系统在无真实收款的情况下自动将订单标记为"已付款"。
一旦上线:用户下单 → 显示支付成功 → 进入发货流程 → 商户分文未收。
这是 PAY-001,严重级,上线即直接资损。
发现 #2:任意用户可以导走全站客户个人信息
订单详情接口 order/get_order_detail,按 order_id 查询时,后端只验证是否登录,不验证这个订单是不是当前用户的。订单 ID 是连续整数。
用一个零订单账号,order_id 从 1 枚举到 100:
# 脱敏验证:只记录字段是否存在,不输出真实值
def redact(data):
sensitive = ["consignee", "name", "mobile", "phone", "address"]
result = {}
def walk(obj):
if isinstance(obj, dict):
for k, v in obj.items():
if any(s in k.lower() for s in sensitive) and not isinstance(v, (dict, list)):
result[k] = f"<非空:{len(str(v))}字符>" if v else "<空>"
walk(v)
elif isinstance(obj, list):
for item in obj[:2]: walk(item)
walk(data)
return result
结果:每一个订单的收货人姓名(3字符)、手机号(11字符)、收货地址(20+字符)全部返回。
一个刚注册的普通用户,不需要任何特殊权限,改一个数字,就能把全站所有客户的姓名电话地址导走。
这是 SEC-002,严重级,触及《个人信息保护法》,上线即大规模信息泄露。
发现 #3:10个人同时访问就会卡死
性能压测用阿里云机器(同区低延迟)做梯度测试:
|---------|------------|----------------|-----------|---------|
| 并发数 | 静态资源吞吐 | 动态页吞吐 | P99延迟 | 超时数 |
| 5 | 2314 req/s | 11.5 req/s | 1.65s | 0 |
| 10 | 正常 | 11.6 req/s | 3.52s | 1 |
| 20 | 正常 | 10.5 req/s | 4.45s | 2 |
| 30 | 正常 | 10.1 req/s | 4.45s | 9 |
静态资源完全健康,动态页吞吐从并发5开始就锁死在约11 req/s,加再多并发也上不去,延迟线性膨胀。
SSH 进服务器看:CPU 全程 88~99% 空闲,PHP 进程池正常,问题出在 Node SSR 进程------pm2 jlist 一看:exec_mode: fork_mode, instances: 1。
单进程、单线程、串行处理所有 SSR 请求。 10个人同时访问就会排队等待。
修复方法:改一行配置,fork 模式改 cluster 模式,多核全部跑起来。整改成本极低,但不改上线就是灾难。
这是 PERF-001,严重级。
三、越权全量审计:系统性问题的挖掘方式
发现 SEC-002 之后,我没有停在这一条漏洞上。IDOR 这类问题的本质是架构层面的设计缺陷,如果一个接口没有归属校验,同类接口大概率也没有。
Claude Code 帮我梳理了所有涉及"按对象 ID 操作"的接口:
提示给 Claude Code:已知订单详情接口按 order_id 查询时无归属校验,请根据已捕获的接口列表,找出所有可能存在相同问题的接口------凡是请求体包含 order_id、aid、Storeid、partner_id、user_id 等对象标识参数的写/读接口,都需要验证。
Claude Code 生成了批量验证脚本框架,我补充了业务逻辑,跑完之后发现了整个越权矩阵:
|-----------|-----------|----------|--------------------|
| 编号 | 接口 | 漏洞类型 | 风险 |
| SEC-010-1 | 订单改址 | IDOR写 | 任意用户改他人订单收货地址 |
| SEC-010-2 | 视力档案删除 | IDOR写 | 任意用户删他人视力健康记录 |
| SEC-011 | 商户提现账户设置 | 免鉴权写 | 匿名修改商户提现银行卡 |
| SEC-012 | 商户结算/提现查询 | 水平越权 | 普通消费者账号可读任意商户的提现流水 |
| SEC-013 | 课程/目录删除 | 鉴权可绕过 | 硬编码常量"鉴权",等同无鉴权 |
其中 SEC-013 最有意思。开发方说"已加验证",我们核查源码:
// del_user_course --- 开发方添加的"验证"
if($param['btn_config_id'] != 207){
return ApiReturn::r(0, [], '按钮验证失败');
}
// 验证通过后:直接操作,无登录校验,无归属校验
Db::name('free_user_course_list')
->where('aid', $param['aid'])
->update(['is_del' => 1]);
btn_config_id = 207 是一个对所有用户永远固定的常量。任何人从正常请求里抓一次包就能得到这个值,带上它就能无限绕过所谓的"验证"。
端到端验证(零损害) :用完全匿名的请求(无 token、无 Cookie),带上 btn_config_id=207,向我自建测试账号的一条课程发起删除请求------该课程的 is_del 从 0 变为 1,被匿名删除成功。(测试用自建数据,删后已恢复。)
四、一个"发现即撤销"的案例:SEC-009 从严重降为低危
源码审计时,我在搜索功能的存储过程里发现了这段代码:
// 搜索历史写入 --- 嫌疑代码
$sql = "INSERT INTO search_history (uid, keyword) VALUES ($uid, '$keyword')";
Db::execute($sql);
$keyword 直接拼接进 SQL,经典存储型 SQL 注入嫌疑,理论上严重级。
但测试人员的责任不是"发现源码嫌疑就报严重",而是验证这段代码是否真的可以被触发。
我追踪路由:/api/m9306/{hash} 这个 hash 是否存在有效路由?找遍了前端调用和后端路由配置,这段代码没有任何前端入口、也没有路由注册。
黑盒验证:尝试枚举可能的 hash 值,均返回 404 或"接口不存在"。
结论:这是死代码,当前版本根本跑不到。 SEC-009 从"中危可利用 SQL 注入"降级为"低危 · 死代码清理提示"。
这个案例体现了一个原则:源码嫌疑需要黑盒验证,发现了异常就报严重是不负责任的。 同样,发现无法利用就撤销,也是测试的应有之义。
五、UX 体验评估:自动化截图的效率优势
UX 评估传统做法是手工调整浏览器窗口,截图存档,边截边记录。这个流程用 Playwright 完全可以自动化:
# 三视口批量截图
for width, height, label in [(1440, 900, "desktop"), (768, 1024, "tablet"), (375, 812, "mobile")]:
page.set_viewport_size({"width": width, "height": height})
page.goto(BASE_URL + path, wait_until="networkidle")
page.screenshot(path=f"artifacts/ux/{name}-{label}.png", full_page=True)
一个脚本,三个视口,所有页面截图全部产出, 不需要手动调整浏览器窗口、不会因为截图顺序不同导致状态不一致。
通过多视口截图发现的问题举例:
- UX-001:桌面横向导航在 1249px 宽度(主流笔记本分辨率)退化为移动端汉堡菜单。1440 下看不出来,768 下一目了然。
- UX-006:门店"去预约"点击后弹出的是纯留资表单,无时间选择、无预约确认------功能被标记为"预约",实际是留资,名实不符,用户预期落差极大。
- UX-008:个人中心在 375px 移动端两栏布局不折叠,侧边栏挤压内容区,标题按钮全部断字、内容不可用。768 和 1440 下完全正常。
最终整理出 9 条 UX 缺陷,每条都附有对应分辨率截图,引用了 Nielsen 十大可用性原则中的具体条款。这种引用标准原则的写法,让甲方和开发方都很难用"这是审美问题"来回避。
六、报告的"三阶段交付"策略
这个项目我设计了分阶段交付而非等所有工作结束后一次性交付:
08-26 开始测试
08-28 第一阶段报告(严重/高危,18项) ← 先交,让开发方立即处理
09-03 第二阶段报告(越权全量+性能+UX,累计32项)
09-07 第三阶段终版(优先级矩阵+合规评级+复测验收框架)
09-15 复测反馈报告(开发方整改后逐项验证)
分阶段交付的核心逻辑:PAY-001(支付假收款)和 SEC-002(客户信息泄露)这两个问题,等到 14 天后才告诉开发方是不负责任的。提前两天交出去,开发方有更多时间整改,甲方也能更早做决策。
Claude Code 在报告写作上的价值体现得很具体:把终端日志翻译成人话。
测试脚本输出的是这个:
{"param": "order_id", "id": 2, "http": 200, "code": 1,
"pii_leak": true, "fields": {"consignee": "<非空:3字符>", "mobile": "<非空:11字符>", "address": "<非空:25字符>"}}
报告里需要写的是这个:
以一个当前账号下无任何商城订单的测试账号(用户ID: ×××××)登录,向 order/get_order_detail 接口发送 {"order_id": 2},接口返回 HTTP 200、业务 code=1,响应体中包含收货人姓名(非空,3字符)、手机号(非空,11字符)、收货地址(非空,25字符)。以上字段明确属于他人订单的个人信息,当前账号无该订单的任何访问权限。
从 JSON 到专业表述,大量重复性"翻译"工作由 Claude Code 完成,我做的是审核和修正------确认每一句话和原始证据一致。
七、复测:脚本让结论无法被质疑
整改之后的复测,是整个流程里最能体现脚本化价值的阶段。
开发方说"SEC-010-1 订单改址已修复"。我的复测脚本直接给出可判定结论:
[基线] 改我自己订单地址 address_id=30
own_order id=1234, addr=30 => code=1 msg=设置成功 ✓ 正常业务路径可用
[越权对比] address_id=不存在(不真正写入),看是否越过归属校验
own_order id=1234, addr=不存在 => code=0 msg=地址不存在 ✓ 符合预期
other_order id=1, addr=不存在 => code=0 msg=该订单数据已不存在 ✓ 归属校验已加上
other_order id=2, addr=不存在 => code=0 msg=该订单数据已不存在 ✓ 归属校验已加上
结论:SEC-010-1 确认修复。
同一个脚本,在修复前跑是这样:
other_order id=1, addr=不存在 => code=0 msg=地址不存在 ✗ 未区分归属,越权未阻断
修复前后的差异就在这一行返回值。这种对照无可辩驳,比截图清晰得多,也无法被质疑"你测的是不是同一个接口"。
而 SEC-002(订单详情读越权)开发方同样说"已修复",复测脚本给出的结论是:
order_id=1, pii_leak=true, fields: {consignee: 非空, mobile: 非空, address: 非空}
还是泄露。深入查才发现:开发方修的是 order_sn(订单号)这条查询路径,但 order_id(订单数字 ID)这条路径完全没动。
逐接口打补丁、漏掉同族接口------这是本次整改暴露出的系统性问题。
八、最终结论:3个P0未清零,不建议上线
复测结论汇总:
|-----------|-------------------|------------|----------------|
| 编号 | 问题 | 开发方标注 | 复测结论 |
| SEC-010-1 | 订单改址越权 | 已处理 | ✅ 确认修复 |
| SEC-010-2 | 视力档案删除越权 | 已处理 | ✅ 确认修复 |
| SEC-002 | 订单详情越权读 | 已处理 | ❌ 不通过,仍可越权 |
| SEC-011-1 | 商户提现账户匿名改 | 已处理(禁用) | ❌ 不通过,接口仍匿名可达 |
| SEC-013 | 免登录删他人课程 | 无需处理(已加验证) | ❌ 不通过,硬编码常量可绕过 |
| SEC-012 | 商户端 partner_id 越权 | 已处理(禁用) | 部分修复,同族接口仍漏 |
| PAY-001 | 支付未真实收款 | 无需处理 | 转 APP 上架后线上验证 |
| PERF-001 | 动态页 SSR 串行 | 已处理 | 待压测确认 |
P0 三条线仍有缺口,维持"暂不建议上线"判断。
九、效率对比:AI协作 vs 传统方式
|------------|---------------------|-------------------------|------------|
| 环节 | 传统方式 | Claude Code 协作 | 提升幅度 |
| 建立接口地图(侦查) | 手工翻 DevTools,2~4h | 脚本自动捕获,20min | 6~12倍 |
| 单个漏洞脚本编写 | 手写调试,0.5~2h | Claude生成骨架+微调,10~30min | 3~6倍 |
| 脚本报错调试 | 查文档翻 Stack Overflow | 直接贴错误给 Claude,分钟级 | 10倍+ |
| 多视口 UX 截图 | 手工调窗口截图,30min/页 | 脚本批量,2min/页 | 15倍 |
| 日志→缺陷描述 | 手工整理,1~2h/条 | Claude 翻译+人工审核,20min/条 | 3~6倍 |
| 复测结论判定 | 主观判断,易被质疑 | 脚本对照输出,客观可审计 | 质的提升 |
整体工期:14工作日完成,传统估算6~8周。 不是因为少测了什么------31项缺陷、4个维度、3套子系统、150+接口、3份报告+1份复测报告------该有的一项不少。
十、几条值得记录的方法论
10.1 零损害原则必须固化在脚本里
生产环境测试,"零损害"不能靠临场记忆,必须在脚本设计层面强制:
- 读操作:redact() 函数强制脱敏,绝不输出真实 PII
- 写操作:优先用 id=999999999(不存在的 ID),触发返回但不写库
- 必须落库时:只操作自建测试账号的自建测试数据,测完即清理
凭证(token、cookie)不硬编码进脚本,通过环境变量注入,不进版本库。
10.2 每个安全发现都要完成"端到端验证"
源码里看到危险写法,不等于漏洞可利用。SEC-009 的案例说明:发现嫌疑 → 追踪路由 → 黑盒验证可达性 → 确认利用面,这个完整链条缺一不可。跳过验证直接报严重,是不负责任的;验证发现不可达,勇于撤销,也是专业素养。
10.3 复测比首测更依赖脚本
首测可以靠手工操作加直觉,但复测需要和首测做精确对照------同一个操作序列、同一个参数组合、同一个判断标准。脚本把这些固化下来,让"整改有没有效"成为可客观验证的问题,而不是测试方和开发方各说各话的口水仗。
10.4 报告结构决定沟通质量
把"开发方标注"和"测试方结论"放在同一张表里,争议直接呈现,甲方看一眼就知道哪里有分歧。结论用4态(确认修复 / 不通过 / 部分修复 / 条件待验证)而非 Pass/Fail,能更准确表达"修了一半"这种真实情况。"上线闸门"单独成段,直接写"P0未清零不建议上线",不给任何模糊解读空间。
10.5 AI 适合做的和不适合做的
适合:生成脚本骨架、调试报错、批量生成截图、把 JSON 翻译成文字、检查同族接口覆盖、辅助整改建议措辞。
不适合:判断某个发现是否构成真实威胁(需要业务理解)、确认整改是否真正有效(需要端到端验证)、确定上线建议(需要全局视角和承担责任)。
最危险的用法:把 AI 生成的脚本和结论不经审核直接执行或交付。AI 加速了你做事的速度,测试人员仍然对每个结论负责。
尾声
这次项目结束后,甲方拿到了一份可以背书的检测报告,开发方拿到了带优先级的整改矩阵,复测给出了明确的通过/不通过结论。整个链路:发现 → 证据 → 报告 → 整改 → 复测 → 上线判断,每一步都有文档和脚本支撑,可以在6个月后翻出来逐条核对。
这件事靠纯手工也可以做,但可能需要两个月、两个人才能完成。Claude Code 没有替代测试工程师的判断,但它把测试工程师的生产力乘以了几倍,让一个人在合理时间内完成原本需要一个团队才能做的工作量。
这就是新的测试方式和传统方式之间,最本质的差距。