代码审查进阶:我用 3 个 LLM 揪出 2 个博客安全漏洞

发布于 阅读 56未分类

代码审查进阶:我用 3 个 LLM 揪出 2 个博客安全漏洞

一、为什么选博客这个小项目做"互喷辩论"的完整验证?

上篇(代码审查谁说了算?我让 3 个 LLM 互喷到一方服)发出去后,有读者问我:

"你那套'互喷辩论'方法论,有没有真的走完整流程?"

这个问题问到痛点了。上篇我确实用了一个中型项目做实战演示——让 2 个 LLM 出报告,Claude Code 复盘辩论,最后生成整改方案。但那次项目比较大,e2e 测试跑一遍要不少时间,所以文章重点放在"方法论设计"上,实施验证部分写得简略。

这次我特意选了博客这个小项目,原因很实在:

  1. 项目小,改起来快
  2. api-test 跑一遍很快(394 个 case 不到2秒就跑完)
  3. 所有接口都可以本地验证,测试结果可以直接验证

这意味着我可以完整走完整个流程

3 个 LLM 出报告 → Claude Code 辩论 → 二次复核 → 实施修改 → 跑测试对比

而不只是"出 fix-plan 就完事"。

所以这篇算是上篇的"完整版"——同样的"互喷辩论"方法论,但这次真的走完了从"审查"到"修复验证"的全流程。

本文就是这次完整实战的记录。


二、为什么是 e2e 测试框架?

选 cases.yaml 作为审查对象,有三个原因:

1. 它是"测试我的测试"——元审查

cases.yaml 是我的博客后端项目的 e2e 测试用例清单,394 个 case、17 个 phase,覆盖所有 API 端点、所有认证矩阵、所有边界值。

问题在哪?

  • case 是我自己写的,可能有盲区
  • note 字段标注的"已知 bug / 现状说明"可能过期(代码修了,但 note 没更新)
  • 框架不支持的断言可能被静默吞掉("摆设测试")

让 LLM 审 cases.yaml,相当于"测试我的测试"——如果 case 本身有 bug,那 394/394 绿灯就是"虚假的安全感"。

2. 它是"多 LLM 协作"的绝佳试验场

cases.yaml 是 YAML 格式,394 个 case,每个 case 有 id、method、path、auth、body、expect、note 等字段。

LLM 审查这种文件,正好考验

  • 能不能读懂 YAML 结构
  • 能不能对照后端源码(handle/.go、domain//dao.go、service/*.go)验证断言对不对
  • 能不能找出"摆设测试"、"过期 note"、"覆盖盲点"

这三点是上篇方法论里"互喷辩论"的核心——不是看 LLM 能不能出报告,是看 LLM 能不能拿出证据说服对方

3. 它真的值得审

394/394 绿灯,但我心里没底。因为:

  • cases.yaml 是我几个月前写的,中间后端代码改过好几轮
  • note 字段里标注的"已知 bug"可能已经修了(但我没更新 note)
  • 有些 case 的 expect 可能是"抄"的——我看源码觉得应该返 200,就写了 200,但没真的跑过边界值

这种"没底"的感觉,正是 LLM 审查的价值所在


三、工具选型:3 个新工具进场

上篇用了 workbuddy(auto)和 qoder(qwen3.7-max),这次多加一个:codex(minimax-m3)

最终进场 3 个工具:

工具模型结果备注
codexminimax-m3(好友订阅享9折+Builder权益✅ 跑通新工具,报告最详细(32 个 finding)
qoderqwen3.7-max(每天免费200积分)✅ 跑通上篇用过,这次报告更聚焦(11 个 finding)
workbuddyhy3.0 preview(超便宜,省积分)✅ 跑通上篇用过,这次报告最少(7 个 finding),但抓到了几个独特的点

为什么选这 3 个?

  • codex:新工具,调用的minimax-m3
  • qoder:上篇表现稳定,qwen3.7-max 对 YAML 格式理解不错
  • workbuddy:hy3.0 preview 表现还可以

zcode 再次缺席——这次项目小,glm-5.2 的赠送额度肯定能跑完,但突然发现 zcode 免费额度又不送了,连进场机会都没了。


四、核心方法论:完整流程(这次真的跑完了)

上篇演示了"互喷辩论 + 二次复核"流程,但因为测的是中型项目,e2e 跑一遍太慢,文章重点放在"方法论设计"上,实施验证部分写得简略。

这次我用小项目(博客),终于能把完整流程跑通了

核心方法论:完整流程

这次跟上次最大的不同

  1. 真的跑了 4 个 verifier agent 并行核对——不是"想",是真去 grep + Read 源码
  2. 真的回了二次复核——不是"辩论完就定稿",是让原工具带着辩论记忆再看一次
  3. 真的达成了统一——11/11 个核心 finding 全部三方共识,可以直接进入实施

五、实战记录:三轮迭代的完整产出

第 1 轮:3 个工具出报告

用时:每家几分钟

产出

工具finding 数量状态
codex32 个✅ 跑通,报告最详细
qoder11 个✅ 跑通,报告更聚焦
workbuddy7 个✅ 跑通,报告最少但独特

合计 66 个 finding(去重前),按 P0→P4 分级后:

  • 5 个 P0 真 bug / 摆设测试(含 1 个真生产安全漏洞)
  • 8 个 P1-P2 handler bug / 覆盖盲点(含 1 个真生产安全漏洞 JWT 撤销)
  • 6 个 P3 文档过期 / 维护债务
  • 2 个 P4 风格 / 轻量新增

第一轮的"互喷"还没发生——3 个工具是独立的,各自出报告,谁也不服谁。

第 2 轮:Claude Code 验证 + 辩论 + 生成 fix-plan

这一步是整套流程的核心。Claude Code 配合 external-audit-fix skill,做了这几件事:

  1. 把 66 个 finding 拆给 4 个 verifier agent 并行处理
  2. 每个 agent 亲自去 backend/ 真实代码里 grep + Read
  3. 跟 codex 一对一逐条辩论:每条 finding 都要拿到证据(代码位置 + 反例),拿不出证据就反驳掉
  4. 跟 qoder 一对一逐条辩论:同样标准
  5. 跟 workbuddy 一对一逐条辩论:同样标准
  6. 只有三家 + Claude Code 三方共同认同的 finding,才进 fix-plan

结果

判定数量含义
✅ 三方共同认同21真 bug,需要修
🟡 部分认同3部分对,部分需要纠正
❌ Claude Code 反驳11报告误读源码,不修

最精彩的一幕

qoder 报告里有个 P0 finding:公开文章列表泄露草稿(SEC-1)。

  • qoder 原始报告就抓到了,但 codex 和 workbuddy 都没报
  • Claude Code 翻代码验证:handle/article.go:101-105 GetArticles 无 status 参数时不过滤,P11-03 case 的 oneof: [0, 1] 允许草稿返回
  • 这是真生产安全漏洞——未登录用户能看到草稿

如果没有"多 LLM 协作",这个漏洞可能就这么漏过去了(workbuddy 漏报,codex 一开始也没单独提)。

另一个精彩瞬间

workbuddy 报告里说 A-50 note 过期(service 层手动校验 guest_name max=50,但 note 说"未生效")。

  • codex 说"expect vs note 自相矛盾"
  • qoder 说"note 过期(已修)"
  • Claude Code 翻代码验证:service/comment.go:89 确实有 if len(req.GuestName) > 50 校验

三家各有偏差,最准确的是 workbuddy。这就是"互喷辩论"的价值——不是简单相信某家报告,是真去翻代码 + 用证据说服对方。

这一轮用时约 10 分钟(Claude Code 配合 external-audit-fix skill,自动拆 agent 并行验证)。

⚠️ Token 成本警告:我这次在 Claude Code 验证 3 个文档时使用了 workflow 模式,这个模式果然费 token——就这一个指令下去,用掉了快 30M token。如果你也想试,记得先确认额度够不够。

第 3 轮:二次复核(事后诸葛亮)

这一步是整个流程的"金矿"——把 fix-plan 回传给原 3 个工具,让它们带着辩论时的记忆,再看一次最终结论

codex 二次复核

  • 32 项里,补充了 1 个之前漏报的 finding(SEC-1 公开列表泄露草稿,一轮时没单独提,二轮补回了)
  • 降级了 1 个 finding(FULLTEXT-SQLITE 行为,从 P0 降级到 P2)

qoder 二次复核

  • 11/11 全部一致,0 实质补充
  • 但澄清了 1 个误判:一轮把 AU-05/AU-11/AU-21 note 过期标成 P0,二轮澄清"finding 对但优先级应 P3"

workbuddy 二次复核

  • 7 项完全同意
  • 补充了 2 项有价值的点:
    • P16-06 self 路径 case 设计有问题(PUT /users/profile 不读 URL,u2 调就是改 u2,不存在"意图改 u1")
    • A-29 size=100 不是 bug(base.go:62 硬上限是 100,不是 50)

最终共识

共识强度finding来源
⭐⭐⭐ 全员#1 SEC-1 公开列表泄漏草稿(P0 真安全漏洞)qoder 原始 + codex 二轮补回 + verifier
⭐⭐⭐ 全员#2 FW-1 P14-08/P15-05 摆设测试三方一致
⭐⭐⭐ 全员#3 SEC-2 JWT 撤销codex + codex-re
⭐⭐⭐ 全员#4 HANDLER-1 A-58/A-59 50000→2001codex + codex-re
⭐⭐⭐ 全员#5 HANDLER-PAGE-1 P-17/P-20 50000→40400qoder + codex-re
⭐⭐⭐ 全员#6~#11 其余 6 个 finding三方

11/11 个核心 finding 全部达成强共识,可直接进入实施阶段。


六、核心发现:2 个真生产安全漏洞 + 1 个摆设测试

🔴 P0:公开文章列表泄露草稿(qoder 抓到,codex/workbuddy 漏报)

漏洞描述

handle/article.go:101-105 GetArticles 无 status 参数时不过滤,P11-03 case 的 oneof: [0, 1] 允许草稿返回。

后果

未登录用户(或普通用户)调用 GET /api/v1/articles 能看到草稿(status=0 的文章)。

修复方案

  1. handler 强制公开端点 status=1
  2. cases.yaml 期望改 oneof: [1](只允许已发布文章)

这个漏洞怎么被发现的?

qoder 原始报告就标了 P0-2,但 codex 和 workbuddy 都没报。如果没有"多 LLM 协作",这个漏洞就这么漏过去了。

这就是"互喷辩论"的价值——不是看哪家最强,是看多家加起来能不能覆盖盲区。

🟠 P1:JWT 撤销(codex 抓到)

漏洞描述

middleware/auth.go:15-62 AuthMiddleware 不查 user.Status。banned 用户旧 token 永远有效直到过期。

后果

用户被管理员 banned 后,旧 token 仍然能用(直到过期)。如果用户 token 过期时间是 7 天,那 banned 后还能访问 7 天。

修复方案

  1. middleware 加 status 校验
  2. 新增 U-19/U-20/U-21 三个 case(测试 banned 用户 token 被拒绝)

🟠 P1:P14-08/P15-05 摆设测试(3 家全报)

问题描述

P14-08P15-05 两个 case 用了 body_not_contains 断言,但框架不支持该断言。两个 case 跑了但没断言任何事——等于没测。

后果

cases.yaml 里标了这两个 case 是 ✅ 绿灯,但实际上没做任何断言。394/394 绿灯里,有 2 个是"虚假的绿灯"。

修复方案

  1. run_tests.pybody_not_contains 支持
  2. P14-08/P15-05 case 加真实断言

七、复盘:这次实验最值得记住的事

1. "多 LLM 协作"不是噱头,是真能抓到单 LLM 漏的漏洞

这次最有力的证据:

  • SEC-1 公开列表泄露草稿:qoder 抓到了,codex 和 workbuddy 都没报
  • SEC-2 JWT 撤销:codex 抓到了,qoder 和 workbuddy 都没报
  • HANDLER-PAGE-1 P-17/P-20 50000→40400:qoder 抓到了,codex 和 workbuddy 都没报

没有哪家是"最强"的——每家都有盲区,但三家加起来,盲区就小多了。

2. "互喷辩论"比"投票"更有效

最容易踩的坑是让 Claude Code 当"裁判"——把 3 份报告合并一下,挑哪个对哪个错。

真实做法是"分别辩论":Claude Code 跟每家一对一逐条辩论,每条 finding 都要拿到证据(代码位置 + 反例),拿不出证据就反驳掉。只有三家 + Claude Code 三方共同认同的 finding,才进 fix-plan

这套流程下来,21 个 confirmed finding 全部有代码证据,不是"某家说有 bug"就信。

3. 二次复核是"金矿"

workbuddy 二次复核时补充了 2 项:

  • P16-06 self 路径 case 设计有问题
  • A-29 size=100 不是 bug(base.go:62 硬上限是 100)

如果不让 workbuddy 复核,Claude Code 自己在辩论时也看不出这两点。

LLM 之间的"辩论 + 复核"是这次实验真正的金矿。

4. 经验必须沉淀成工具

上篇做完后,Claude Code 顺带总结了踩坑教训,生成了 external-audit-fix skill。

这次直接用这个 skill 跑,效果显著:

  • 自动拆 4 个 verifier agent 并行核对
  • 自动生成 3 份 fix-plan + 1 份 rectification-report
  • 自动判断"是否达成统一"

"代码能复用,经验也得能复用"——LLM 协作流程尤其需要 skill 化。


八、实施结果:5 个 PR 全部落地

达成统一并生成 fix-plan 后,我真的动手实施了。最终结果

实施完成

  • 最终状态:399/399 测试通过(0 fail / 0 skip)— 比修复前的 394 还多 6 个新 case
  • 5 个 PR 全部落地(在同一个分支 fix/p1-handler-leakage,可按需 cherry-pick 拆分支)

5 个 PR 落地情况

PRcommit内容用例变化
PR-11bccbee真安全漏洞修复:SEC-1 公开列表强制 status=1 + page/tags 错误码修正改 9 case
PR-2f4d198fframework 加 body_not_contains 支持,闭环 P14-08/P15-05 摆设测试改 2 case
PR-339327dc真安全漏洞修复:JWT 撤销 — AuthMiddleware 校验 user.Status改 1 case
PR-465f649c清理 16 条过期 note + A-50 措辞修正删 16 note
PR-5f3977db覆盖盲点:self 路径 / admin 删评论 / emoji 边界+6 case

总改动量:30 个文件 / +6259 行 / -45 行 / 5 个 commit + 1 个 docs commit

实施中的关键发现

  1. SEC-1 修复需强化:初版"未传 ?status 才强制"被 P13-28 绕过(匿名 ?status=0 仍返草稿)。最终方案是无条件覆盖非 admin 的 status 过滤

  2. router 缺 OptionalAuthMiddleware/articles 是纯 public group,admin token 不被 parse。修复 = 路由层加 OptionalAuthMiddleware(),让 handler 能识别 admin role

  3. JWT 撤销需 DB 查询:AuthMiddleware 增加 global.DB.Model(&User{}).Where("id = ?", claims.UserID) 检查 status。增加 settings 开关 auth.skip_status_check 用于测试

  4. workbuddy 修订版更准确:P16-06/P16-07 我原本设计是"u2 改 u1 测越权",但 handler 走 JWT 不读 URL,根本不存在越权。workbuddy 改为"缺鉴权 / 缺必填参数"回归保护,更符合实际 handler 行为

跳过的覆盖盲点(范围说明)

  • 限流 cases:需 runner 改造清计数器(避免 394 case 互相触发限流)
  • CASCADE cases:删除用户/分类/标签会破坏 phase 依赖链,需新建隔离 phase
  • pages 列表分页:handler 不支持(仅 published bool),需先扩 backend
  • multipart 上传:runner 不支持 files= 字段(SKILL.md 已写"推未来")

最终验证

指标修复前(2026-06-22 19:55:49)修复后(2026-06-22 21:53:27)
总用例数394399(+6 新 case)
pass394399
fail00
SEC-1 真安全漏洞❌ 漏洞 + 测试帮凶✅ 已修
P14-08 / P15-05 摆设❌ 跑了但没断言✅ 真断言负向
JWT 撤销❌ banned 旧 token 永远有效✅ AuthMiddleware 校验 status
过期 note 数量16 条误导维护者✅ 删 + 改 A-50 措辞
Self 路径回归保护❌ 未来 handler 改 URL 解析会漏✅ P16-06/07 锁住

详细实施报告external-audits/20260622/implementation-report.md(包含每个 PR 的实施细节、中间踩坑、跳过的范围说明、提交历史、cherry-pick 拆分 PR 的命令)


九、最终测试报告:399/399 全绿 ✅

5 个 PR 全部落地后,跑完整 e2e 测试,结果:

   399 cases, 399 pass / 0 fail / 0 skip
   用时 3.4s

从 394 到 399,多出来的 6 个 case 是:

case来源覆盖什么
CM-38-prepcodex #16准备 admin 待删评论
CM-38codex #16admin 删任意评论
P16-06workbuddy 修订版self 路径缺鉴权 → 40100
P16-07workbuddy 修订版self 路径缺 old_password → 40001
C-25workbuddy #5分类名含 emoji
T-27workbuddy #5标签名含 emoji

之前 394/394 绿灯是虚假的安全感——P14-08/P15-05 跑了但没断言任何事,SEC-1 漏洞一直开着。 现在 399/399,每一盏绿灯都是真的绿


写在最后

整个实验做完,最大的感慨是:

"多 LLM 协作"不是让多个 AI 投票,而是让它们互喷,用证据说服对方。

单 LLM 永远有盲区。但让多个 LLM 互喷 + 用证据说服对方,反而能逼出"哪条 bug 是真的"、"哪条方案更好"。

这次实战验证了上篇提出的"互喷辩论"方法论——真的能揪出生产安全漏洞,真的能达成统一,真的能生成可实施的 fix-plan

如果你也在用 LLM 做代码审查,建议试试这套完整流程:

  1. 3 家出报告(不要只信一家)
  2. Claude Code 复盘辩论(必须翻真实代码,不能"想")
  3. 二次复核(事后诸葛亮环节,ROI 最高的 5 分钟)
  4. 生成 fix-plan + PR 拆分(直接可实施)

最大的成本不是 token,是第一次跑会踩坑——但写进 skill 后,第二次就会更流畅和高效。


本文完整输出物

external-audits/20260622/
├── codex-minimax-m3.md                       # codex 原报告
├── codex-minimax-m3.fix-plan.md              # codex fix-plan(Claude Code 生成)
├── codex-minimax-m3.fix-plan-re.md           # codex 二次复核
├── qoder-qwen3.7-max.md
├── qoder-qwen3.7-max.fix-plan.md
├── qoder-qwen3.7-max.fix-plan-re.md          # qoder 二次复核
├── workbuddy-hy3.0-preview.md
├── workbuddy-hy3.0-preview.fix-plan.md
├── workbuddy-hy3.0-preview.fix-plan-re.md   # workbuddy 二次复核
├── rectification-report.md                    # 一轮综合
├── final-rectification-report.md             # 二轮综合(达成统一)
└── implementation-report.md                  # 实施报告(5 个 PR 落地细节)

实施已完成:5 个 PR 全部落地,399/399 测试通过,P0/P1 漏洞全部修复。

评论 (0)
0 / 500
暂无评论,快来抢沙发~