首页
/ oh-my-claudecode 代码审查基准解读:用 SQL 注入夹具验证 AI 代码审查器的漏洞检出能力

oh-my-claudecode 代码审查基准解读:用 SQL 注入夹具验证 AI 代码审查器的漏洞检出能力

2026-09-08 16:07:47作者:管翌锬

本文以 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.mdcode-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-reviewerquality-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}`);

日志把原始用户输入 qrole 直接输出,若其中包含换行符或控制字符,攻击者可伪造日志行、污染日志分析链路(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 50ORDER BY 列名无法通过占位符绑定,pg 等驱动的参数化能力并不覆盖 SQL 语法片段,所以排序列只能依靠“白名单常量映射”,这是本案例中最易被修复者忽略的技术细节。

五、基准如何“批改”审查结果:ground-truth 与打分机制

想理解这套考题的严谨程度,需要看 three 个共享层文件:

  1. 数据结构层types.ts 定义了 GroundTruthGroundTruthFinding、严重级别(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 个关键词才算对上,防止泛泛而谈蒙混过关。

  2. 匹配与打分层benchmarks/shared/scorer.ts 负责把智能体产出的 findings 与 ground-truth 做关键词匹配,产出 matchedFindings(命中)、missedFindings(漏报)、spuriousFindings(误报),再换算成召回率/误报率/漏报率与复合分。

  3. 解析层parser.ts 中的 parseGenericOutput 从智能体的 Markdown 输出里按 Critical / Major / Minor 小节抽取条目,并用 EVIDENCE_PATTERN 正则检测每条结论是否带代码位置或文件引用——对应 ground-truth 中每个 finding 的 location 字段(如 GET /search:33),这正是“不带证据的结论不得分”的实现机制。

此外还有严格的夹具身份校验:loadGroundTruth(见 runner.ts)会用 validateSharedGroundTruth 校验 JSON,并强制 fixtureIddomain 必须和夹具完全一致。夹具的 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_KEYANTHROPIC_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 项目中承担着三重角色:

  1. 安全教材:完整展示 Node.js + PostgreSQL 场景下从注入、越权到日志污染的最常见缺陷模式,并给出“参数化 + 常量映射白名单 + 鉴权”的可执行修复标准;
  2. 基准考题:以 expectedVerdict: "REJECT" 与 6 条分级 findings 作为自动化判卷标准,量化 code-reviewer 智能体的漏洞检出能力;
  3. 审查哲学样本:印证“发现与过滤分离”(发现阶段不预筛、过滤交给下游)与“带证据、带分级、带修复建议”的输出契约,防止 AI 审查流于“看起来不错”的空话。

若想深入了解智能体的完整审查规则与输出契约,可继续阅读 agents/code-reviewer.md;想了解评分口径与关键词匹配阈值,可研读 types.tsbenchmarks/shared/scorer.ts;要跑通整套评测,可参考同一目录下 benchmarks/run-benchmark.py 等顶层编排入口。以本夹具为参照,读者可以将任意“埋雷”代码段改造成同样的 fixture + ground-truth 结构,为自己的代码审查智能体建立可持续回归的质量基线。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
docsdocs
暂无描述
Markdown
900
5.83 K
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
860
1.35 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
927
1.85 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.84 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
533
603
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.37 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
397
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.04 K
525