Claude Security 插件实测:让一组代理把代码库扫一遍找漏洞,再出补丁给你审
Claude Security 是官方的深度扫描插件:几个代理分头读代码找漏洞,再由三个评审各自投票,通过的才进报告。本站在一个埋了问题的小应用上跑了两次:都找到了路径穿越,还找出一个我们没埋的崩溃问题并验证属实;但两次的结论数量和评级不一样,出补丁两轮都被它自己的评审否决。合计 3.70 美元。
本站已实测,测试日期:2026-10-12。
这篇教程解决一个问题:想对整个仓库,或者一个分支的改动,做一次像安全研究员那样的检查。不是改代码时顺手查一下,而是专门花一段时间把它过一遍。
Claude Code 有一个官方插件做这件事,叫 Claude Security。它和本站之前写过的安全检查插件不是一个东西,先把几样分清楚。
| 什么时候 | 工具 | 管什么 |
|---|---|---|
| 写代码的当下 | 安全检查插件(security-guidance) | Claude 写出来的代码里的常见漏洞,当场改掉 |
| 想查的时候,单次 | /security-review 命令 |
对当前分支做一遍安全检查 |
| 想查的时候,深度 | Claude Security 插件(这一篇) | 多个代理扫整个仓库或一段改动,结论经过独立评审,可以再出补丁 |
| 提 PR 的时候 | Code Review(Team 和 Enterprise 套餐) | 带着完整代码库背景审 PR |
| CI 里 | 你已有的静态分析和依赖扫描 | 规则明确的检查 |
文档的说法是这几层叠着用,谁也不替代谁。
| 实测环境 | |
|---|---|
| 日期 | 2026-10-12 |
| 版本 | Claude Code 2.1.296,Claude Security 插件 0.12.0 |
| 系统 | Ubuntu 24.04,Python 3.12.3,Node.js 22 |
| 模型 | 会话用 Sonnet 5.5 |
| 连接方式 | 通过酷站AI 中转站,不是直连官方 |
| 被扫的代码 | 一个 6 个文件、93 行的 Node.js 小应用,是本站测安全检查插件时留下的,里面有故意埋的问题 |
要准备什么
文档列的前提:
- 付费套餐、API 访问,或者第三方云厂商接入。扫描要用到动态工作流来编排它的代理。
python33.9 以上。插件的脚本只用标准库,不会往你机器上装东西。- Git,扫改动和出补丁要用。只做全库扫描的话,没有版本控制的目录也行。
模型不用另外配。干活的代理用的就是你会话当前的模型,想换就在开扫之前 /model。文档提到有几个辅助步骤固定用 sonnet 这个别名。
安装
会话里输入:
/plugin install claude-security@claude-plugins-official
本站用的是终端里的写法,装到当前项目:
claude plugin install claude-security@claude-plugins-official --scope project
✔ Successfully installed plugin: claude-security@claude-plugins-official (scope: project)
装到项目范围,会在项目里生成一个 .claude/settings.json,里面记着启用了这个插件。这个文件后面会惹一点麻烦,先记住它。
卸载:claude plugin uninstall claude-security。
跑一次全库扫描
插件只加了一个命令:/claude-security。输入后出来一个菜单:
What would you like to do?
❯ 1. Scan codebase (Recommended)
Find vulnerabilities across the whole repository or a scoped part of it.
2. Scan changes
Find vulnerabilities in a branch's or pull request's diff, or one specific commit.
3. Suggest patches
Turn reported findings into verified patch files you review and apply yourself.
三件事:扫整个代码库、扫一段改动、给已经报出来的问题出补丁。
选第一项。它先读了一遍仓库的文件清单,然后问范围:
Which part of the repository should be scanned?
❯ 1. Whole repository (~6 files, `medium`) (Recommended)
The tree is small, so the whole thing can be read in one scan.
2. Scoped scan — one area
3. I don't know — you choose
仓库大的时候,这里会建议你一次只扫一块,比如接口层或者登录相关的代码。
选完范围是一道确认:
This scan may take a while and may use a significant number of tokens. You will need to leave
Claude Code open while the scan completes. Are you sure you want to continue?
确认之后才真正开始。扫描在后台跑,输入框下面有一条进度:
◯ scan ▰▰▰▰▰▰▰▰▱▱▱▱▱▱▱▱▱▱▱▱ 2/5 · 1m10s · ↓ 34.2k tokens
它开头提醒了一句:这个插件在 auto 模式下最顺,否则代理每走一步都要你批准。
五个阶段
第二次扫描时,Claude Code 弹出了运行动态工作流的确认框,里面列了这条流水线的五个阶段:
| 阶段 | 做什么 |
|---|---|
| Inventory(清点) | 把仓库分成几个组件,每个顶层目录要么扫,要么明确说明跳过 |
| Threat model(威胁建模) | 每个组件一个建模的代理 |
| Research(研究) | 按“组件 × 漏洞类别”分格子,每格一个研究员 |
| Sweep(补漏) | 把上一步没覆盖到的地方补一遍 |
| Panel(评审) | 三个评审从三个角度各自投票:能不能到达、影响多大、有没有已有的防护 |
评审这一步是它和“让 Claude 看一眼有没有漏洞”最大的区别。只有评审通过的才写进报告。
结果长什么样
第一次扫描用了 5 分 35 秒。结束时它在对话里给了摘要(原文是英文,下面是大意):
扫描完成:2 个发现通过了验证,都是高危,都在 app.js。
- F2,/files 的路径穿越(app.js:23)。未登录的人可以读到进程能读的任何文件,比如
GET /files?name=../config/payment.js。- F1,远程让进程崩溃(app.js:32)。查询参数没有校验,处理函数没有 try/catch,一个不带参数的
GET /go就会抛出未捕获的异常,进程退出。同一行还会重定向到你给的任何地址。3 个评审都确认了这两条。还有第三个候选,config/payment.js:3 里写死的值,被 3 个评审里的 2 个否决,认为是占位符。如果它是真的密钥,请轮换,因为 F2 能读到这个文件。
扫描过程中没有执行任何代码,每一条结论都是读代码得出的。
每次扫描在仓库里新建一个带时间戳的目录:
CLAUDE-SECURITY-20261012-141902/
├── CLAUDE-SECURITY-RESULTS.md 报告,给人看的
├── CLAUDE-SECURITY-RESULTS.jsonl 同样的内容,一行一条,给程序用
├── CLAUDE-SECURITY-RESULTS.sarif SARIF 格式,GitHub 代码扫描等工具能读
├── CLAUDE-SECURITY-REVISION-….json 这份报告对应哪个提交、什么力度、验证得多彻底
└── .gitignore 内容只有一个 *
那个 .gitignore 是有意放的:不管你怎么 git add,报告都不会被顺手提交进去。要把报告留在历史里做审计记录,删掉这个 .gitignore 再提交。
报告里每条发现有固定的几段:影响、位置和 CWE 编号、具体是什么、怎么利用、前提条件、怎么修、评审的投票结果。开头还有一节“覆盖范围”,写明读了哪些文件、哪些没读、为什么。
它找得准不准
这个应用是本站自己埋的雷,所以可以对答案。
| 问题 | 是不是我们埋的 | 第一次扫描 | 第二次扫描 |
|---|---|---|---|
/files 路径穿越 |
是 | 报了,高危 | 报了,高危 |
/go 开放重定向 |
是 | 并在崩溃那一条里提了一句 | 单独一条,低危 |
| 不带参数的请求让进程崩溃 | 不是 | 报了,高危 | 报了,中危 |
config/payment.js 里写死的密钥 |
是(一个假的,以 _0000 结尾) |
列为候选,被评审否决,认为是占位符 | 没有进报告,出补丁时被评审顺带提到 |
/whoami 把请求头里的内部令牌原样返回 |
是 | 没报 | 没进报告,出补丁时被评审顺带提到 |
三点观察。
一、它找出了一个我们没埋的问题。 进程崩溃那条不在我们的清单里。报告说它是读代码推出来的,没有运行过。本站自己动手验证了一下:把应用跑在本机回环地址上,发一个不带参数的 GET /go:
TypeError [ERR_HTTP_INVALID_HEADER_VALUE]: Invalid value "undefined" for header "Location"
at ServerResponse.writeHead (node:_http_server:439:8)
at /go (app.js:32:9)
进程退出了。结论属实。路径穿越和开放重定向也一并验证了,都能复现。
二、对那个假密钥的判断是对的。 它确实是个占位符。评审没有把它算成漏洞,但在摘要里留了一句“如果是真的就轮换”。这比一看到 sk_live_ 就报高危要有用。
三、/whoami 没有报。 这个路由把调用者自己发来的请求头返回给调用者,严格说泄露不了别人的东西,不报也说得过去。但它是一个标着“上线前删掉”的调试路由,人来审的话多半会提一句。
两次扫描,结果不一样
同一份代码(第二次只多提交了一个设置文件),同样的范围和力度:
| 第一次 | 第二次 | |
|---|---|---|
| 发现数量 | 2 | 3 |
| 路径穿越 | F2,高危 | F1,高危 |
| 进程崩溃 | F1,高危 | F2,中危 |
| 开放重定向 | 并在 F1 里 | F3,低危 |
| 用时 | 5 分 35 秒 | 约 7 分钟 |
最要紧的那条两次都在,评级也一样。差别在边上:同一个问题是并成一条还是拆成两条,崩溃算高危还是中危,编号也换了。
文档对此是明说的:扫描不是确定性的,同一份代码扫两次可能报出不同的东西。它的建议是定期扫,并且用那份版本记录把每份报告和它对应的提交、设置对上号。
实际用的时候记住两点:别拿编号当长期的引用(“修一下 F1”要带上是哪一份报告);一次没报不等于没有。
出补丁:两轮都被它自己否决了
报告出来后,可以让它为选中的发现出补丁。本站只要了路径穿越那一条。
先碰到的坑:工作区不干净
第一次要补丁,它拒绝了:
F2 的补丁我先不做,因为前提条件没满足:那份扫描报告的版本戳里
revision.dirty是 true。扫描时工作区里有未提交的内容,现在看到的是未跟踪的.claude/目录。补丁是针对已提交的代码生成的。
那个没提交的 .claude/ 目录,就是安装插件时生成的设置文件。
所以按项目范围装这个插件之后,先把 .claude/settings.json 提交掉(或者加进 .gitignore),再扫描。 否则扫描能跑,但那份报告出不了补丁,得重新扫一遍。本站就是这样多花了一次扫描的钱。
顺带一提,报告文件名里会带上这个状态:第一份的版本记录叫 …-dirty.json,第二份没有这个后缀。
然后:补丁没有通过评审
提交干净、重新扫描之后,补丁流程跑了起来。每个补丁先在仓库的一份临时副本里起草,然后交给另外的代理评审。它跑了两轮,最后的结论:
路径穿越没有出补丁。评审两轮都没放行。
- 第一轮:用
fs.realpath做目录限定,修掉了穿越。评审否决了它,因为改变了可接受的输入:name=readme.txt/从 404 变成了 200,uploads/ 里指向外部的符号链接从 200 变成了 404。- 第二轮:改成纯字面的检查,这两处差异都消失了。评审仍然否决,理由是补丁里的输入检查顺带修了另一条发现里的崩溃问题,超出了这一条的范围。
- 两轮的共同点:穿越本身都被堵住了,对抗评审也没找到补丁引入的新攻击路径。
按流程,第二次被否决就不再重试,所以没有 .patch 文件。
patches/ 目录里留下的是说明,不是补丁:
patches/
├── F1.md 为什么没出补丁、被否决的那一版改了什么、报告原本的修复建议
├── PATCHES.md 索引,以及评审顺带看到、但不属于这一条的其他问题
└── patches.jsonl 机器可读的记录,status 是 declined
文档里写着补丁要过三关才会写出来:确实修了这一条、没有引入新的漏洞、除此之外行为不变。过不了就给你一张说明。本站碰到的正是第三关:两版都把洞堵上了,但都“多做了一点”。
这个标准很严。好处是它交出来的补丁可以放心地单独审、单独合;代价是像这次这样,两个问题挨在同一行代码上时,单修一个很难不碰到另一个。它自己给的建议也是这个:把路径穿越和崩溃两条放在一起出补丁。本站没有再跑这一轮。
补丁不管出没出,它都不会自动应用。文档反复强调这一点:补丁写在 patches/ 下,要你自己 git apply,并且建议每个补丁单独开一个 PR。
花了多少,用了多久
| 用时 | 花费估算 | |
|---|---|---|
| 第一次扫描(含菜单) | 5 分 35 秒 | 1.00 美元 |
| 第二次扫描,加两轮出补丁 | 约 25 分钟 | 约 2.70 美元 |
| 整个会话合计 | 36 分 31 秒 | 3.70 美元 |
全程用的是 Sonnet 5.5。合计的用量:输出 14.7 万 token,读缓存 830 万,写缓存 55.5 万,98% 的输入来自缓存。
这是 93 行代码的价钱。仓库大了,研究阶段的格子(组件数乘以类别数)会成倍增加。文档在确认框里写的“可能用掉大量 token”不是客套话。先在一个小范围上试一次,看清楚单价再放大。
这是 Claude Code 按官方价格估算的数字,不是中转站实际扣的金额。
通过中转站用,碰到的两件事
整个流程通过中转站跑通了,动态工作流、后台代理都正常。中途有两处和接入方式有关的现象:
- 扫到一半弹出一个通知,要按回车才继续显示。 内容是 auto 模式的分类器计费方式在调整,而“你的请求经过的地址不兼容这次更新”,所以这个会话仍按原来的方式计费。它不影响功能。但如果你走开了,回来会发现界面停在这个通知上。
- 出补丁时有一次命令被拒,界面提示
Classifier unavailable。 auto 模式靠一个分类器决定放不放行,那一下它没响应,命令就被拒了。代理自己重试之后继续了。
另外,文档提到辅助步骤用 sonnet 这个别名。如果你的中转站上模型名和官方不一样,要用 ANTHROPIC_DEFAULT_SONNET_MODEL 把别名指到一个能用的模型上。
用下来的建议
- 先提交,再扫描。 尤其是按项目范围装插件之后那个
.claude/settings.json。 - 用 auto 模式。 否则几十个代理的每一步都要你点。
- 开扫之前定好模型。 想省钱用 Sonnet;本站没有比较过不同模型找到的东西差多少。
- 报告里的结论自己验证一遍再动手修。 它声明了没有运行代码。这次三条都属实,但那是这一次。
- 扫了没报,不等于没有。 两次扫描的差别就在眼前。它是多一层检查,不能代替依赖扫描和人工审查。
- 紧挨着的几个问题,一起出补丁。
这次实测没有覆盖到的
- 只在一个 93 行的小应用上试过。 大仓库上的耗时、花费、分块扫描都没有数据。
- “扫改动”那一项没有试:扫一个分支的差异、一个 PR、一个提交。
- 没有成功拿到一个补丁,所以补丁文件长什么样、
git apply之后的效果没有看到。把两条合在一起出补丁那一轮没有跑。 - 只用了 Sonnet 5.5,中等力度。 换模型、换力度的效果没有比较。
- SARIF 文件没有导进 GitHub 试。
- 只扫了两次。 “不确定性”有多大,两次说明不了。
- 每条结论只对照了我们自己埋的问题。 这个应用里有没有两次都没被发现、我们自己也不知道的问题,无从判断。