← 中文资源

简体中文版本

字符串拼接导致 SQL 注入 — 使用参数化查询

通过以参数化查询替代字符串拼接、校验输入,并对标识符使用白名单,消除 SQL 注入。

阅读英文原文

症状

本篇调试指南针对“字符串拼接导致 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`;

验证

重发注入探测并确认不返回多余行;验证合法查询仍可用且日志显示占位符。

修复后补充一条回归测试,复现原始症状,防止问题再次出现。

这类故障通常是环境问题而非纯逻辑错误,因此在缺陷记录中应写明完整命令、环境变量与工作目录。请用最小的脚本复现,待最小用例清楚后再扩大范围。为这类故障准备一份简短的排查清单,能显著减少下次排障时间。