症状
本篇调试指南针对“字符串拼接导致 SQL 注入 — 使用参数化查询”。内容涵盖可观察的症状、可复现的诊断路径、根本原因、具体修复方式,以及如何验证修复并防止回归。
可观察到的症状包括:
- 形如 ’ OR 1=1 – 的输入返回了意外行
- 含引号的用户名破坏了查询
- 日志显示 SQL 中直接包含用户输入
诊断
先在隔离环境中复现故障,再按顺序执行以下检查,逐步缩小范围。
步骤 1
curl -i "https://api.example.com/search?q=%27%20OR%201%3D1%20--%20"
返回多余行或结果顺序改变,即证明用户输入未转义地进入查询。
步骤 2
grep -rn "'+.*\|${.*}" src/ | grep -i "select\|insert\|update\|delete"
每处以拼接构建的查询都是候选;应以绑定参数替换。
常见诱因(按出现频率排序):
- 把用户输入拼接进 SQL 字符串
- 参数在模板字符串中插值,而非绑定
- 动态标识符(表/列名)来自用户输入且无白名单
根因
拼接输入时 SQL 文本与数据共用同一通道,数据库无法区分代码与数据。攻击者可插入成为语句一部分的 SQL,从而执行任意子句。
修复
对所有值使用参数化查询(由驱动绑定占位符);对不能参数化的标识符,在插值前按严格白名单校验。
注意:ORM 并非天然安全;若拼接值,raw 或字面量片段仍可能被注入。
// ❌ concatenation -> injection
// const sql = `SELECT * FROM users WHERE email = '${email}'`;
// ✅ parameterized query (values are bound, never parsed as SQL)
const { rows } = await db.query(
'SELECT id, email FROM users WHERE email = $1',
[email],
);
// dynamic identifier: allowlist, never interpolate raw input
const COLS = new Set(['created_at', 'name']);
const col = COLS.has(sort) ? sort : 'created_at';
const q = `SELECT * FROM users ORDER BY ${col} LIMIT $1`;
验证
重发注入探测并确认不返回多余行;验证合法查询仍可用且日志显示占位符。
修复后补充一条回归测试,复现原始症状,防止问题再次出现。
这类故障通常是环境问题而非纯逻辑错误,因此在缺陷记录中应写明完整命令、环境变量与工作目录。请用最小的脚本复现,待最小用例清楚后再扩大范围。为这类故障准备一份简短的排查清单,能显著减少下次排障时间。