第 25 章 代码安全审查实战
前面章节讲的
/code-review与/security-review是 diff 级别的守门员;本章讲的是仓库级安全审查——拿到一个完整项目,如何在 1-2 小时内产出一份可信、可处置、风险分级统一的安全报告。素材全部来自真实审查,没有虚构案例。
25.1 结论:安全审查的核心是"维度并行 + 统一分级"
用 Claude Code 做仓库级安全审查,核心工作流只有四步:
Plan Mode 建立项目事实模型 → 并行子代理按维度审 → 统一风险分级 → 汇总报告这条工作流之所以有效,是因为它解决了安全审查的三个根本矛盾:
- 维度矛盾:安全审查不是"通读代码",而是按鉴权、注入、配置、供应链、崩溃等正交维度分别扫。一个子代理一次只盯一个维度,覆盖率远高于"让 Claude 看一下这个项目安全不安全"。
- 分级矛盾:不同项目、不同审查者用的分级命名五花八门(P0/P1/P2 vs CRITICAL/HIGH/MEDIUM vs HIGH/MEDIUM/P0),导致横向不可比。本章给出一套统一定义。
- 可处置矛盾:发现 100 个问题但不排优先级,等于没发现。风险分级的价值不是分类,而是决定先修什么。
本章所有案例数据来自四份真实审查:new-api(AI 网关)、CodeGraph(本地代码工具)、NTrace-core(网络追踪工具)、Clash Verge Rev(代理客户端),覆盖四类典型项目。
25.2 审查工作流四步详解
25.2.1 第一步:Plan Mode 建立项目事实模型
目标:在改任何代码之前,先让 Claude 只读理解项目,输出一份事实模型。这一步用 Plan Mode(第 07 章),严禁直接开审。
事实模型至少包含五项:
| 维度 | 要回答的问题 | 为什么要先搞清楚 |
|---|---|---|
| 系统入口 | HTTP 路由、CLI 命令、MCP 工具、WebSocket 端点各有哪些 | 入口决定攻击面 |
| 信任边界 | 哪些代码处理外部输入(URL/订阅/用户提交/网络响应) | 越过信任边界的地方就是审查重点 |
| 数据流 | 凭据、Token、用户数据怎么存、怎么传、怎么销毁 | 凭据生命周期是高危区 |
| 权限模型 | 认证怎么做、授权怎么做、默认凭据是什么 | 鉴权 fail-open 是最高频高危 |
| 依赖与构建 | 安装脚本、CI 配置、二进制下载、依赖锁定 | 供应链是常被忽略的一维 |
以 NTrace-core 为例,事实模型阶段会识别出:入口有 CLI、HTTP server、WebSocket、MCP 四类;信任边界包括用户指定的 trace 目标、第三方 IP 地理 API 响应、ICMP 原始包解析;凭据有 NextTrace API Token(写文件)、ipleo Token(硬编码)。这些信息直接决定了后面要派几个子代理、各盯什么。
25.2.2 第二步:按维度拆分并行子代理
目标:把审查拆成正交维度,每个子代理只盯一维,并行跑。这一步是第 19 章 Subagents 与第 20 章 Workflows 的实战预告,这里先用结论。
典型维度拆分(适用于大多数项目):
| 子代理 | 审查范围 | 典型高危模式 |
|---|---|---|
| 鉴权与认证 | 登录、Token、Session、CORS、CSRF | fail-open、默认凭据、CORS 反射、常量时间比较 |
| 注入与解析 | SQL、FTS、命令、正则、YAML/TOML、包解析 | 拼接查询、ReDoS、边界检查缺失、Zip Slip |
| 配置与凭据 | secrets、TLS、文件权限、默认值 | InsecureSkipVerify、0644 凭据文件、硬编码 Token |
| 供应链与构建 | 安装脚本、CI 权限、依赖锁定、二进制下载 | curl|sh 无 checksum、write-all、未 pin SHA |
| 崩溃与资源 | nil 解引用、数组越界、无界缓存、goroutine 泄漏 | 无 nil 检查、io.ReadAll 无 LimitReader、channel 写入竞态 |
new-api 的审查就是这套编排的范本:主代理负责架构与依赖,4 个子代理分别盯安全后端、代码质量、前端、架构,最终汇出 4 HIGH + 6 MEDIUM + 4 P0 + 6 P1。关键不是派了 4 个,而是 4 个的边界不重叠——鉴权归安全后端,nil panic 归代码质量,前端 new Function() 归前端,这样不会漏也不会重复。
25.2.3 第三步:统一风险分级
目标:把所有子代理的发现按统一标准分级。这一步最容易被忽略,却是横向可比的前提。详见 25.3。
25.2.4 第四步:汇总报告
目标:合并所有子代理输出,去重、排优先级、给处置建议。报告必须包含:
- 项目概览(技术栈、规模、核心功能)
- 问题汇总表(按级别计数)
- 逐项清单(级别 + 文件:行号 + 问题 + 修复建议)
- 正面评价(项目做得好的地方,避免只挑毛病)
- 优先修复建议(分"立即修复"与"高优先"两档)
为什么必须包含正面评价:安全审查不是找茬,是建立信任。把项目做对的地方记录下来,既能校准审查者的判断(知道这个团队的安全意识水平),也能给修复者信心。NTrace-core 的审查就列了 13 条正面评价——subtle.ConstantTimeCompare、Cookie HttpOnly+SameSite、Token 文件 0600、sanitizeLogParam 日志防注入——这些做对了的地方,说明团队知道该怎么做,问题是执行不一致。
25.3 风险分级体系:CRIT/HIGH/MED/LOW/P0
四份真实审查用了三套命名,这是行业现状的缩影。本章强制统一为五级:
| 级别 | 定义 | 处置优先级 | 典型例子(来自真实审查) |
|---|---|---|---|
| CRITICAL | 可被远程利用导致 RCE/SSRF/凭据泄露/完全控制,或安装即中招 | 立即修复,阻断发布 | curl|sh 无 checksum、SSRF 可探元数据端点、CSP 完全禁用、硬编码共享 Token |
| HIGH | 需特定条件触发但有实质危害,或高危路径上缺一道防线 | 本 sprint 修复 | CORS+Credentials 并用、Zip Slip、InsecureSkipVerify、无认证 WebSocket |
| MEDIUM | 需链式利用或影响有限,或防御纵深缺失 | 排入 backlog | 弱 PRNG、Cookie 无 MaxAge、日志文件 0644、new Function() 计费表达式 |
| LOW | 代码异味、效率问题、信息泄露轻微 | 顺手修 | 正则每次编译、命名不一致、依赖可精简 |
| P0(崩溃) | 非安全但会导致 panic/进程退出/服务不可用 | 与 CRITICAL 同优先级 | nil 解引用、数组越界、向已关闭 channel 写入 |
P0 单列的的理由:安全漏洞与崩溃是两类问题,但处置优先级相同——都会让服务不可用。new-api 的 4 个 P0(nil panic)虽然不是安全漏洞,但一旦触发就是 500 错误,比一个 MEDIUM 的 CORS 配置更该先修。
分级标准必须统一:本章四份审查里,Clash Verge Rev 用 P0/P1/P2/P3,NTrace-core 用 CRITICAL/HIGH/MEDIUM/LOW/P0,导致横向对比困难。正确做法是审查前先在 Plan 里写死分级定义,所有子代理引用同一张表,杜绝"我觉得这个是 HIGH"的主观判断。
25.4 按项目类型分组的审查重点与典型案例
不同类型的项目,攻击面截然不同。下面四类覆盖了大部分实战场景,每类配一个真实案例。
25.4.1 AI 网关:new-api(附 sub2api 对照)
项目:QuantumNous/new-api — 下一代 LLM 网关,聚合 40+ AI 提供商 技术栈:Go 1.25.1 | Gin | GORM | Redis | React 19 + React 18 规模:74 controller,58 service,41 model,42 Provider 适配器
AI 网关类审查重点:
| 维度 | 重点 | new-api 的真实发现 |
|---|---|---|
| 鉴权 | Token 校验是否检查状态/过期/配额 | TokenAuthReadOnly 未检查 → HIGH(S4) |
| 跨域 | CORS 与 Credentials 组合、WebSocket Origin | AllowAllOrigins + AllowCredentials 并用 → HIGH(S1);WS CheckOrigin 全 true → HIGH(S2) |
| TLS | 是否允许禁用验证 | TLS_INSECURE_SKIP_VERIFY 环境变量可关 TLS → HIGH(S3) |
| 默认凭据 | 初始密码、Session Secret | 默认 root/123456 → MEDIUM(S8);SESSION_SECRET="random_string" 仅警告 → MEDIUM(S9) |
| 前端执行 | 计费表达式是否用 eval/Function | new Function() 执行计费表达式 → MEDIUM(S6) |
崩溃项(P0):4 个,集中在计费与用户控制器——GetTokenById 失败后仍访问 token 字段(两处 nil panic)、id.(int) 无 nil 检查、defer tx.Rollback 后显式 Commit 逻辑混乱。
处置建议:先修 B1/B2/B3(nil panic 直接导致 500)→ 再修 S4(鉴权 fail-open)→ S1/S2(跨站)→ S3(TLS)。sub2api 同类项目的主要风险是"部署 secrets 默认空",与 new-api 的"默认弱密码"是同一类问题——AI 网关的默认配置普遍偏宽松,审查时务必逐项检查默认值。
25.4.2 代理客户端:Clash Verge Rev v2.5.2
项目:基于 Tauri 2 + React 的 Clash Meta(mihomo)GUI 代理客户端 规模:~97,000 行(Rust 后端 ~19,000 行 + TypeScript 前端) 分级:5 P0 + 16 P1 + 28 P2 + 8 P3(按本章标准映射为 5 CRIT + 16 HIGH + 28 MED + 8 LOW)
代理客户端类审查重点:
| 维度 | 重点 | 真实发现 |
|---|---|---|
| 前端隔离 | CSP 是否启用、文件 scope 是否收窄 | CSP 完全禁用("csp": null)→ CRIT;文件系统 scope 用 ** → CRIT |
| 供应链 | CI 权限、Actions pinning、二进制 checksum | permissions: write-all → CRIT;Actions 未 SHA pin、构建脚本下载无 checksum → HIGH |
| 解压安全 | 备份/订阅解压是否防路径遍历 | 备份 Zip 解压 Zip Slip → HIGH |
| 远程内容 | 订阅 URL 是否防 SSRF、脚本沙箱是否完整 | 远程订阅缺 SSRF 防护 → HIGH;Boa 脚本沙箱隔离不完整 → HIGH |
| 凭据存储 | WebDAV/订阅凭据存哪 | WebDAV 凭据明文存 localStorage → HIGH;WebDAV 硬编码 danger_accept_invalid_certs(true) → HIGH |
处置建议:代理客户端的攻击面与前三个类型完全不同——它的核心风险是"前端能干什么"。CSP 禁用 + 文件 scope ** 意味着一旦有任何 XSS(Markdown rehype-raw、SVG 图标都是入口),前端就能读写任意文件。修复顺序:先开 CSP + 收窄 scope → 再堵 XSS 入口 → 最后修供应链。
25.4.3 本地代码工具:CodeGraph v0.9.2
项目:@colbymchenry/codegraph — 本地代码智能系统(tree-sitter + SQLite + MCP) 语言:TypeScript(Node.js),~48K 行 分级:10 CRITICAL + 27 HIGH + 40 MEDIUM + 30 LOW + 4 P0
本地工具类审查重点:
| 维度 | 重点 | 真实发现 |
|---|---|---|
| 安装脚本 | curl|sh / irm|iex 是否有 checksum | install.sh 与 install.ps1 均无校验 → 2 CRIT |
| 查询注入 | FTS/SQL 是否参数化 | FTS5 查询注入(特殊字符过滤不全)+ 引号包裹绕过 → 2 CRIT |
| 正则安全 | 解析器正则是否防 ReDoS | svelte/vue/liquid [\s\S]*? 回溯 + C++ 检测正则 → 2 CRIT |
| 资源边界 | 缓存是否有淘汰 | MCP projectCache 无界增长 + Resolution 7 个 Map 无 LRU → 2 CRIT |
| 配置解析 | YAML/TOML 是否用标准库 | Hermes 手工 YAML 解析器 + TOML 未转义控制字符 → 2 CRIT |
正面评价(值得记录):SQL 全部参数化查询无注入;validatePathWithinRoot 路径防护完善;O_NOFOLLOW 防 /tmp 符号链接攻击;clamp() 限制所有 MCP 工具输入范围;TypeScript strict 模式。这说明团队安全意识不差,问题集中在"自己造轮子"——手工 YAML 解析器、手工 TOML 序列化器、自己写正则解析器,这些都该换标准库。
处置建议:本地工具的"安装即中招"是最优先级——install.sh/install.ps1 加 SHA256 校验是阻断级修复。其次堵 FTS5 注入(白名单替代黑名单)。ReDoS 和无界缓存可排第三档。
25.4.4 网络追踪工具:NTrace-core
项目:NTrace-core — NextTrace 网络追踪工具 语言:Go ~27,302 行 分级:11 CRITICAL + 22 HIGH + 24 MEDIUM + 18 LOW + 8 P0
网络工具类审查重点:
| 维度 | 重点 | 真实发现 |
|---|---|---|
| SSRF | 用户指定目标是否限制内网 | normalizeTarget 接受任意 IP/域名,可探 169.254.169.254 → CRIT(C2);环境变量控制完整 URL → CRIT(C9) |
| 认证 | server 模式是否要 Token | WebSocket 无认证 + 无连接数限制 → CRIT(C3);缓存清除端点无保护 → CRIT(C11) |
| TLS | 第三方 API 调用是否验证证书 | 多处 InsecureSkipVerify: true → CRIT(C4) |
| 凭据 | Token 怎么存怎么传 | 明文 0644 文件 → CRIT(C5);硬编码 ipleo="NextTraceDemo" → CRIT(C8);Token 在 URL Query → CRIT(C10) |
| 包解析 | 原始包是否有边界检查 | ICMP/TCP 解析缺长度校验 → CRIT(C7),同时也是 P0 崩溃 |
| MCP | 暴露操作是否有权限控制 | MCP 接口无细粒度权限 → CRIT(C6) |
崩溃项(P0):8 个,全是边界检查缺失——ICMP 解码 nil 解引用、TCP 探测数组越界、MTR Runner nil channel、macOS net.ParseIP 返回 nil 后 .To4()、IPInfo strings.Fields(...)[0] 越界、DN42 PTR CSV row[3] 无边界。Go 的 nil panic 和切片越界是 P0 高发区,审查时凡是 data[offset:]、arr[i]、x.Field 链都要盯。
处置建议:网络工具的 SSRF 是阻断级——服务端 trace 接口必须做内网 IP 过滤(RFC 1918/4193/6598)+ 速率限制。硬编码 Token 与 InsecureSkipVerify 是"执行不一致"的典型:项目在 Token 文件安全上做对了 0600、符号链接拒绝、原子写入,却在另一处硬编码共享 Token,说明审查必须覆盖所有凭据路径,不能只看一处就放心。
25.5 /code-review、/security-review 与深度人工审查的分工
Claude Code 提供两个 Bundled Skill(第 13 章)与本章的深度审查形成三层防线:
| 层级 | 工具 | 范围 | 适用场景 | 局限 |
|---|---|---|---|---|
| 第一层 | /code-review | 当前 diff | 每次提交前 | 只看变更,不看全局;质量向+少量安全 |
| 第二层 | /security-review | 待提交变更 | 安全敏感改动 | 仍是 diff 级,覆盖维度有限 |
| 第三层 | 本章工作流 | 整个仓库 | 接手新项目、发布前审计、定期复审 | 耗时长(1-2 小时),需人工编排 |
分工原则:/code-review 和 /security-review 是日常守门员,拦的是"这次改动有没有引入新问题";本章工作流是全身体检,查的是"这个项目整体安全水位如何"。两者不可互相替代——只跑 /security-review 就认为项目安全,等于只看了今天改的几行,漏掉了所有历史遗留问题。
实际用法:日常开发每 PR 跑 /code-review --fix;涉及鉴权/凭据/网络的改动加跑 /security-review;接手新项目或发布大版本前,用本章工作流做一次全量审查。三层配合,才叫工程化。
25.6 并行子代理编排(预告第 19/20 章)
本章的并行审查依赖 Subagents(第 19 章)与 Workflows(第 20 章),这里给出最小可用编排:
主代理(Plan Mode):
1. 建立项目事实模型
2. 按维度拆分子代理任务
3. 派发并行子代理(鉴权/注入/配置/供应链/崩溃)
4. 等待全部返回
5. 汇总去重,统一分级,输出报告编排要点:
- 子代理任务要带维度定义:不要说"审一下安全",要说"审鉴权维度:检查登录、Token、Session、CORS、CSRF,引用 25.3 分级表"。
- 子代理只读:审查阶段不允许子代理改代码,改了就会污染事实模型。修复是另一阶段。
- 子代理返回结构化:要求每条发现带
级别 + 文件:行号 + 问题 + 修复建议,主代理汇总时才不用重新读代码。 - 主代理负责去重:鉴权子代理和注入子代理可能都报告了同一个 SQL 拼接,主代理要合并成一条。
- worktree 隔离(可选):对超大型项目,可用 worktree(第 19 章)让每个子代理在独立工作树里跑,避免互相干扰。
new-api 的审查就是这套编排的实战:主代理管架构与依赖,4 个子代理分头跑,最终汇成一份统一报告。关键不是派了几个子代理,而是边界不重叠、分级标准统一。
25.7 失败边界
以下三种做法会让审查失效,务必避免:
失败一:只跑 /security-review 就认为安全。/security-review 是 diff 级,只看待提交变更,不看历史遗留。一个有 10 个 CRITICAL 的项目,只要这次 diff 没碰那些代码,/security-review 会干净通过。这是最常见的错觉——"工具没报错=项目安全"。正确认知:diff 级审查拦新增,仓库级审查查存量,缺一不可。
失败二:审查不读真实代码,凭记忆判断。审查记忆文件(如本章引用的四份)是 point-in-time 观察,标注了"47 天前"——代码可能已经改了。用 Claude Code 做审查时,必须让它 Read 真实代码、Grep 真实模式,而不是问"你记得 new-api 有什么安全问题吗"。记忆的作用是提供审查清单(知道要查哪些维度),不是提供审查结论。一旦结论脱离真实代码,就是幻觉。
失败三:风险分级标准不统一。四份真实审查用了三套命名,导致无法横向比较"new-api 和 NTrace-core 哪个更危险"。正确做法:审查前在 Plan 里写死本章 25.3 的五级定义,所有子代理引用同一张表。凡是子代理自己发明分级(比如"严重/中等/轻微")的,主代理汇总时必须映射回统一标准。
边界之外:Claude Code 的安全审查能覆盖 80% 的常见模式(注入、鉴权、配置、崩溃、供应链),但以下场景仍需人类专家:加密协议实现、侧信道攻击、内核态代码、零日漏洞利用。承认边界,把 AI 审查当作"大幅提升覆盖率的初筛",而非"替代专家的终审"。
25.8 小结
仓库级安全审查的四步工作流——Plan 建模、维度并行、统一分级、汇总报告——本质上是把"安全"这个模糊概念,拆成了机器可检查的正交维度和可比较的离散级别。这与第 06 章的核心心法一脉相承:你定维度和分级,Claude 做扫描和初筛。
四类项目的审查重点各异,但底层模式相通:AI 网关盯鉴权 fail-open 与默认配置,代理客户端盯前端隔离与供应链,本地工具盯安装脚本与查询注入,网络工具盯 SSRF 与包解析边界。掌握这些模式,遇到新项目就能快速套用。
最后强调一句:安全审查的价值不在发现多少问题,而在先修哪个。一份分级统一、优先级清晰的报告,远比一份列了 100 条没排序问题的清单有用。这是工程纪律,不是技术能力。
上一章:第 24 章 RAG 知识库实战