问题根源
把用户输入直接拼进 SQL 字符串,攻击者在输入里写 ' OR '1'='1 就能改变语义,读出、篡改甚至删掉数据。
根因解:参数化查询
把「代码」和「数据」分开传:占位符 ? 或命名参数让数据库始终把它当数据,永远拼不进命令。
// 错误:字符串拼接
db.query("SELECT * FROM u WHERE name = '" + name + "'")
// 正确:参数化
db.query("SELECT * FROM u WHERE name = ?", [name])
两个常见错觉
- ORM 一定安全:用原生 SQL 拼接的 ORM 同样危险,raw query 也要参数化;
- 转义就够:手动转义容易漏、易错,参数化更稳。
实战案例:三个依然常见的注入面
- ORM 的 raw 查询:ORM 本身不是护身符,只要写了
raw("... WHERE id=" + id)就照样能被注入。凡是拼接字符串的地方都要改成绑定参数。 - LIKE 与排序字段:
LIKE '%"+kw+"%'会注入;ORDER BY "+col的参数化在很多驱动里不被支持,必须用白名单映射字段名,绝不能直接拼。 - 二次注入:用户输入先转义后入库,读取时又被拼进新 SQL——此时数据里的引号已“洗白”。根因仍是拼接,参数化才是解。
常见问题(FAQ)
参数化能防住所有注入吗?能防住“数据被当成命令”,但如果表名、列名本身来自用户输入,仍需白名单校验。转义函数为什么不够?它与字符集、驱动实现强相关,稍有不一致就会漏,而参数化把边界交给驱动处理。只读账号就不用防了吗?读也能造成数据泄露,且一旦被写成写操作就晚了;应遵循最小权限并始终参数化。日志里能打 SQL 吗?可以,但要打参数化模板与参数值,别拼接后再打,避免日志二次流入其它系统。
纵深防御:把注入的影响面压到最小
- 最小权限:应用账号只授予必要的库表与 DML 权限,禁止
DROP、FILE与跨库查询,即使被注入也难以造成毁灭性后果; - 统一数据访问层:把数据库访问收敛到一层封装,禁止业务代码直接拼 SQL,从流程上消灭拼接入口;
- 代码评审与静态检查:把“字符串拼接 SQL”列为必查项,用静态规则扫描
raw(、字符串加法与模板插值; - 输入白名单而非黑名单:对枚举类参数(排序字段、状态值)直接用白名单映射,不接受自由文本;
- 监控与告警:对异常查询模式(大量
UNION、SLEEP、报错激增)设告警,注入尝试通常在成功前会先留下痕迹。
边界场景的取舍
- 动态表名 / 列名:参数化无法覆盖,必须用白名单映射;不要试图用转义解决;
- 批量插入:使用驱动提供的批量接口(如
VALUES (?,?),(?,?)的参数数组),而不是循环拼字符串; - 模糊搜索:把通配符加在参数值里(
"%" + kw + "%"作为参数),而不是拼进 SQL 文本; - 存储过程:内部若仍用字符串拼接动态 SQL,同样存在注入,需一并审查。
事后处置
一旦怀疑被注入:先保全日志与流量证据,再立即轮换数据库凭据并缩小权限,随后审计近期数据变更与出网连接,最后才是修代码与补测试。顺序颠倒会破坏证据链。
测试与回归方法
- 用例库:建立包含引号、注释符、联合查询片段与编码变体的固定测试集,随接口一起回归;
- 边界字段优先:排序字段、表名映射、模糊搜索参数是最容易漏检的位置,应单独写测试;
- 自动化扫描:把 SQL 注入扫描接入 CI,能在合并前发现新增的拼接写法;
- 线上验证:上线后用只读查询确认应用账号无法执行 DDL,验证最小权限确实生效。
修复优先级
发现注入点后,先堵住对外可触达的入口(尤其是无需登录即可调用的接口),再排查内部接口;同时轮换可能已泄露的凭据、审计近期异常查询。修复顺序应是“止血 → 取证 → 根因修复 → 补测试”,避免先改代码导致现场被破坏。