oh-my-claudecode 代码审查基准解读:用 SQL 注入夹具验证 AI 代码审查器的漏洞检出能力
本文以 oh-my-claudecode 仓库中的 benchmarks/code-reviewer 代码审查基准为背景,围绕其 SQL 注入审查夹具 benchmarks/code-reviewer/fixtures/code/code-sql-injection.md 展开。该夹具是一段刻意植入多种安全缺陷的 Express.js + PostgreSQL 用户搜索接口代码,用于检验 oh-my-claudecode 的 code-reviewer(代码审查智能体)能否像资深工程师一样,定位 SQL 注入、越权删除、日志注入等问题并给出分级、可修复的建议。读完本文,你将掌握:夹具代码中每一处漏洞的原理与正确修复姿势、ground-truth 基准答案如何约束智能体输出、以及如何运行这套代码审查基准来量化审查质量。
一、夹具的定位:它是一道“代码审查考题”
code-sql-injection.md 不是一段需要被直接运行的业务代码,而是 oh-my-claudecode 代码审查基准中的一道评测夹具(fixture)。夹具目录结构如下:
- 待审查代码样本(fixtures):
benchmarks/code-reviewer/fixtures/code/code-sql-injection.md,同一目录还包含code-payment-refund.md、code-retry-handler.md两个样例; - 参考答案(ground-truth):
benchmarks/code-reviewer/ground-truth/code-sql-injection.json; - 评测脚本:run-benchmark.ts;
- 被评测的旧版提示词档案:
benchmarks/code-reviewer/prompts/quality-reviewer.md。
评测流程在 run-benchmark.ts 中定义:run-benchmark.ts 会把夹具全文封装成用户消息 Review the following code for quality, security, and correctness issues:\n\n${fixtureContent},分别喂给 code-reviewer 与 quality-reviewer 两个智能体提示词,再把两者的回复与 ground-truth 自动比对打分。因此,这道“考题”的判定标准就是:代码审查智能体必须发现题目中真实存在的漏洞,而不是凭空挑剔。
二、待审查代码:一个典型的“看着能跑、实则千疮百孔”的接口
以下完整代码来自夹具文档,两个路由覆盖了用户搜索与用户软删除两个功能,读者可先自行审查,再看后文的参考答案:
import express from 'express';
import { Pool } from 'pg';
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const router = express.Router();
interface SearchResult {
id: number;
username: string;
email: string;
role: string;
created_at: Date;
}
/**
* GET /api/users/search?q=<query>&role=<role>&sort=<field>&order=<asc|desc>
* Search users by username or email with optional role filter and sorting.
*/
router.get('/search', async (req, res) => {
const { q, role, sort, order } = req.query;
if (!q || typeof q !== 'string' || q.length < 2) {
return res.status(400).json({ error: 'Query must be at least 2 characters' });
}
// Build the search query
let sql = `SELECT id, username, email, role, created_at FROM users WHERE username LIKE '%${q}%' OR email LIKE '%${q}%'`;
// Apply role filter if provided
if (role && typeof role === 'string') {
sql += ` AND role = '${role}'`;
}
// Apply sorting
const allowedSortFields = ['username', 'email', 'created_at'];
const sortField = sort && allowedSortFields.includes(sort as string) ? sort : 'username';
const sortOrder = order === 'desc' ? 'DESC' : 'ASC';
sql += ` ORDER BY ${sortField} ${sortOrder}`;
// Limit results
sql += ' LIMIT 50';
try {
const result = await pool.query(sql);
const users: SearchResult[] = result.rows;
// Log search for analytics
console.log(`User search: q="${q}" role="${role}" results=${users.length}`);
return res.json({
results: users,
total: users.length,
query: q,
});
} catch (err) {
console.error('Search failed:', err);
return res.status(500).json({ error: 'Search failed' });
}
});
/**
* DELETE /api/users/:id
* Soft-delete a user account.
*/
router.delete('/:id', async (req, res) => {
const userId = req.params.id;
try {
await pool.query(`UPDATE users SET deleted_at = NOW() WHERE id = ${userId}`);
console.log(`User ${userId} soft-deleted`);
return res.json({ success: true });
} catch (err) {
console.error('Delete failed:', err);
return res.status(500).json({ error: 'Delete failed' });
}
});
export default router;
表面看,这段代码有参数校验(q 必须为至少 2 个字符的字符串)、有排序字段白名单、有 LIMIT 50 防全表拉取、还有 try/catch 错误处理,像模像样。但它犯了 Node.js + SQL 开发中最致命的一条禁忌:把用户输入用字符串模板直接拼进 SQL。
三、逐项漏洞拆解(依据 ground-truth 参考答案)
对应夹具的参考答案位于 benchmarks/code-reviewer/ground-truth/code-sql-injection.json。它声明了 expectedVerdict: "REJECT"(结论:应打回)、domain: "code",并给出 6 条分级 findings。下表为完整映射:
| 编号 | 严重级别 | 问题 | 位置(ground-truth 标注) |
|---|---|---|---|
| SQL-CRIT-1 | CRITICAL | 搜索查询 LIKE '%${q}%' 字符串拼接导致 SQL 注入 |
GET /search:33 |
| SQL-CRIT-2 | CRITICAL | role 过滤器 AND role = '${role}' 二次注入 |
GET /search:38 |
| SQL-CRIT-3 | CRITICAL | DELETE 路由把 req.params.id 直接拼入 SQL |
DELETE /:id:67 |
| SQL-MAJ-1 | MAJOR | 破坏性接口无任何鉴权/授权中间件 | DELETE 全路由 |
| SQL-MAJ-2 | MAJOR | 把未净化的用户输入写进日志,存在日志注入 | GET /search:53 |
| SQL-MIN-1 | MINOR | sortField 虽有白名单但仍被拼接进 SQL | GET /search:42-44 |
1. SQL-CRIT-1:搜索关键词注入(主要注入点)
let sql = `SELECT ... FROM users WHERE username LIKE '%${q}%' OR email LIKE '%${q}%'`;
q 来自 req.query,仅校验了“非空、是字符串、长度 ≥ 2”,随后被直接插值进 SQL。攻击者提交 q='; DROP TABLE users; -- 这类 payload 即可操纵整条语句的语义,实现任意 SQL 执行。由于查询无事务包裹、无权限收敛,注入可造成读库、改库乃至删库。参考答案的修复要求非常明确:必须改用参数化查询(parameterized query / prepared statement),即 pool.query(sql, params) 形式,让数据库驱动负责转义。
2. SQL-CRIT-2:role 过滤器的第二个独立注入向量
if (role && typeof role === 'string') {
sql += ` AND role = '${role}'`;
}
注意这与 SQL-CRIT-1 是两个互相独立的漏洞——即使开发者只修了搜索关键词那一处,role 仍可作为第二入口被独立利用。参考答案强调其“independently exploitable”(可独立利用)。这也是基准设计上的用心之处:期望审查智能体不要把两处同类问题合并成一条结论后遗漏其一。
3. SQL-CRIT-3:DELETE 路径参数注入
await pool.query(`UPDATE users SET deleted_at = NOW() WHERE id = ${userId}`);
req.params.id 同样未做类型/白名单校验便直接拼接。攻击者构造 /api/users/1 OR 1=1 之类的 URL,可使 WHERE id = 1 OR 1=1 恒真,从而批量软删除全部用户。path 参数常被开发者默认为“可信输入”,这正是它在参考答案中被单列为第三条 CRITICAL 的原因。
4. SQL-MAJ-1:破坏性操作缺少鉴权
即便修复了注入,该 DELETE 接口仍不完整:它对任何未认证请求开放,无认证中间件、无角色/权限校验。参考实现事实来自 oh-my-claudecode 的 code-reviewer 提示词中的安全审查清单(见 agents/code-reviewer.md 的 ### Security 小节),其中明确要求核查 “Authentication/authorization properly enforced”(认证/授权是否正确强制)。一个会让任意人软删任意账号的接口,即使没有注入也不该被批准合入。
5. SQL-MAJ-2:日志注入
console.log(`User search: q="${q}" role="${role}" results=${users.length}`);
日志把原始用户输入 q 与 role 直接输出,若其中包含换行符或控制字符,攻击者可伪造日志行、污染日志分析链路(log injection)。修复思路是把用户输入在写入日志前做清洗或长度/字符集限制,或使用结构化日志字段。
6. SQL-MIN-1:白名单校验虽好,拼接方式可更健壮
const allowedSortFields = ['username', 'email', 'created_at'];
const sortField = sort && allowedSortFields.includes(sort as string) ? sort : 'username';
这段代码做对了一半:排序字段用白名单兜底,天然阻断了 ORDER BY 注入,值得在“Positive Observations”中表扬。但参考答案仍将其列为 MINOR,理由是字段仍以插值方式进入 SQL;更稳的做法是维护一张「白名单值 → 列标识」的映射对象,只把映射后的常量放进 SQL。这反映了审查中“低危不等于可忽略”的分级哲学:发现阶段保留每条证据,过滤交给下游。
四、修复版实现:参数化查询 + 常量白名单 + 鉴权兜底
依据 ground-truth 中 SQL-CRIT-1/2/3 的说明与 oh-my-claudecode 的安全审查清单,给出可直接落地的安全版本(核心变化是 pool.query(sql, params)):
router.get('/search', async (req, res) => {
const { q, role, sort, order } = req.query;
if (!q || typeof q !== 'string' || q.length < 2) {
return res.status(400).json({ error: 'Query must be at least 2 characters' });
}
// 1) 所有动态值以占位符传入,由 pg 驱动完成转义
const whereClauses: string[] = [
`(username ILIKE $1 OR email ILIKE $1)`,
];
const params: unknown[] = [`%${q}%`];
if (role && typeof role === 'string') {
params.push(role);
whereClauses.push(`role = $${params.length}`);
}
// 2) ORDER BY 不允许参数化列名 -> 用映射对象,只拼接常量
const sortColumns: Record<string, string> = {
username: 'username',
email: 'email',
created_at: 'created_at',
};
const sortField = sortColumns[sort as string] ?? 'username';
const sortOrder = order === 'desc' ? 'DESC' : 'ASC';
const sql =
`SELECT id, username, email, role, created_at FROM users ` +
`WHERE ${whereClauses.join(' AND ')} ` +
`ORDER BY ${sortField} ${sortOrder} LIMIT 50`;
const result = await pool.query(sql, params);
// 3) 日志脱敏后再写入
const sanitizedQ = String(q).replace(/[\r\n]/g, ' ').slice(0, 200);
console.log(`User search: q="${sanitizedQ}" results=${result.rows.length}`);
return res.json({ results: result.rows, total: result.rows.length, query: q });
});
DELETE 路由同理:先校验 id 为纯数字,再改参数化 UPDATE users SET deleted_at = NOW() WHERE id = $1 AND deleted_at IS NULL,并在路由层挂载鉴权中间件(如 requireAuth + requireRole('admin'))。需要强调的是,由于 LIMIT 50、ORDER BY 列名无法通过占位符绑定,pg 等驱动的参数化能力并不覆盖 SQL 语法片段,所以排序列只能依靠“白名单常量映射”,这是本案例中最易被修复者忽略的技术细节。
五、基准如何“批改”审查结果:ground-truth 与打分机制
想理解这套考题的严谨程度,需要看 three 个共享层文件:
-
数据结构层:types.ts 定义了
GroundTruth、GroundTruthFinding、严重级别(CRITICAL | MAJOR | MINOR)等类型,同时给出评分权重SCORING_WEIGHTS(召回 0.25、漏报 0.15、误报 0.10、遗漏覆盖 0.20、视角覆盖 0.10、证据率 0.10、过程合规 0.10),以及关键词匹配阈值MIN_KEYWORD_MATCHES = 2——即审查输出必须命中同一条 finding 至少 2 个关键词才算对上,防止泛泛而谈蒙混过关。 -
匹配与打分层:
benchmarks/shared/scorer.ts负责把智能体产出的 findings 与 ground-truth 做关键词匹配,产出matchedFindings(命中)、missedFindings(漏报)、spuriousFindings(误报),再换算成召回率/误报率/漏报率与复合分。 -
解析层:parser.ts 中的
parseGenericOutput从智能体的 Markdown 输出里按Critical / Major / Minor小节抽取条目,并用EVIDENCE_PATTERN正则检测每条结论是否带代码位置或文件引用——对应 ground-truth 中每个 finding 的location字段(如GET /search:33),这正是“不带证据的结论不得分”的实现机制。
此外还有严格的夹具身份校验:loadGroundTruth(见 runner.ts)会用 validateSharedGroundTruth 校验 JSON,并强制 fixtureId 与 domain 必须和夹具完全一致。夹具的 domain 由目录名推导(code 目录 → code domain,映射见 runner.ts),文件扩展名 .md 或 .ts 均可被识别为夹具。
在「发现与过滤分离」的设计下,agents/code-reviewer.md 要求智能体在审查阶段优先保证覆盖(recall):任何 CRITICAL/HIGH 结论都需同时给出严重级(severity)与置信度(confidence),低置信度的高危项可放入 “Open Questions” 而非直接省略;只有全量发现完成后,才依据“最高置信度高危项”给出 APPROVE / REQUEST CHANGES / COMMENT 三态结论。对应本夹具的 expectedVerdict: "REJECT"——因为存在 3 条 CRITICAL 高危注入,任何合格的审查都必须打回而非放行。
六、运行该基准并量化审查质量
进入仓库根目录后,可执行(需本机具备 Node/tsx 与可用的 ANTHROPIC_API_KEY 或 ANTHROPIC_AUTH_TOKEN,runner 默认模型为 claude-opus-4-6,可用 --model 覆盖):
# 仅验证流水线、不调用 API
npx tsx benchmarks/code-reviewer/run-benchmark.ts --dry-run
# 只跑本夹具、只测 code-reviewer 智能体
npx tsx benchmarks/code-reviewer/run-benchmark.ts \
--agent code-reviewer \
--fixture code-sql-injection
# 完整对比 code-reviewer 与旧版 quality-reviewer
npx tsx benchmarks/code-reviewer/run-benchmark.ts
支持的命令行参数由 runner.ts 解析:--agent/--agents 指定智能体、--fixture 只跑单个夹具、--output-dir 指定报告输出目录、--dry-run 做无 API 的流水线自检。注意智能体提示词加载顺序(runner.ts):先取仓库根 agents 下的同名 code-reviewer.md,找不到再回退到 benchmarks/code-reviewer/prompts/quality-reviewer.md 这类档案目录——这正是本次评测“新版 code-reviewer 吸收 quality-reviewer 之后是否更好”的 A/B 实验基础。运行结束后会输出 JSON 与 Markdown 两份对比报告到 --output-dir(默认 benchmarks/code-reviewer/results),字段包括各夹具的复合分、token 用量、API 延迟与逐条命中/漏报/误报明细。
七、小结:一道夹具的三重价值
通读 code-sql-injection.md 与配套 ground-truth,可以看到它在 oh-my-claudecode 项目中承担着三重角色:
- 安全教材:完整展示 Node.js + PostgreSQL 场景下从注入、越权到日志污染的最常见缺陷模式,并给出“参数化 + 常量映射白名单 + 鉴权”的可执行修复标准;
- 基准考题:以
expectedVerdict: "REJECT"与 6 条分级 findings 作为自动化判卷标准,量化 code-reviewer 智能体的漏洞检出能力; - 审查哲学样本:印证“发现与过滤分离”(发现阶段不预筛、过滤交给下游)与“带证据、带分级、带修复建议”的输出契约,防止 AI 审查流于“看起来不错”的空话。
若想深入了解智能体的完整审查规则与输出契约,可继续阅读 agents/code-reviewer.md;想了解评分口径与关键词匹配阈值,可研读 types.ts 与 benchmarks/shared/scorer.ts;要跑通整套评测,可参考同一目录下 benchmarks/run-benchmark.py 等顶层编排入口。以本夹具为参照,读者可以将任意“埋雷”代码段改造成同样的 fixture + ground-truth 结构,为自己的代码审查智能体建立可持续回归的质量基线。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0632
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
video-shotcraftAI宣传片skill,使用 Remotion 制作电影级产品视频:提供106 张镜头配方卡和可复用的视频魔板。适用于 Claude Code 与 Codex以及所有其他智能体Markdown00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python09
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00