FlawOpen

flawopen.com/SQL Injection/数字 ID

为什么字符串格式化对数字 ID 也很危险?

参考页面
简答

风险与值在现实中的含义无关,而在于查询是通过拼接文本构造的。“它始终是数字”只是对数据的假设,并非代码强制保证。

假设失效的情形

HTTP 参数始终以字符串形式到达

URL 查询参数或表单字段在线路上传输时都是文本,与数据库列期望的类型无关。若不显式验证类型,框架不会在代码收到前阻止攻击者发送 id=1 OR 1=1 而不是数字。

后续重构可能悄悄改变值的来源

代码今天可能因 ID 始终来自可信内部来源而安全;如果新调用方把用户提供的值传给同一函数,代码就可能变得不安全。漏洞一直潜伏着,只等这项改动。

类型转换可能掩盖实际值

尤其在动态类型语言中,日志里看起来像数字的值仍可能是包含额外字符的字符串。没有显式验证,预期类型和实际类型并不保证一致。

即使验证了类型,也要对查询进行参数化

按照应用预期的格式验证 ID,然后通过数据库驱动的参数 API 传入。验证保护输入契约;参数化确保输入不会被解释为 SQL 语法。驱动在协议层的行为可能不同,因此不要假设所有驱动都以相同方式传输值。PostgreSQL 参数不能表示表名或列名;动态查询结构应从应用内固定的允许列表中选择。

绑定数值,并验证其业务含义

如果应用要求特定类型,请将 ID 验证为正的安全整数,同时仍将其作为查询值绑定。验证保护应用的输入契约;参数化则确保输入不会成为 SQL 语法。PostgreSQL 参数不能替代表名或列名,因此动态查询结构应从应用内固定的允许列表中选择。

不安全 — 将 ID 插入 SQL 文本
const rawId = new URL(request.url).searchParams.get("id");
const result = await pool.query(
  `SELECT id FROM users WHERE id = ${rawId}`
);
安全 — 验证 ID 并将其作为值绑定
const rawId = new URL(request.url).searchParams.get("id");
if (rawId === null || !/^[0-9]+$/.test(rawId)) {
  return new Response("Invalid id", { status: 400 });
}
const id = Number(rawId);
if (!Number.isSafeInteger(id) || id < 1) {
  return new Response("Invalid id", { status: 400 });
}
const result = await pool.query(
  "SELECT id FROM users WHERE id = $1",
  Array.of(id)
);

参考资料

另请参阅 SQL 注入(CWE-89):原理与各编程语言的防护方法 — flawopen.com 什么是参数化查询? Prisma $queryRaw 能防止 SQL 注入吗? Rails where() 字符串插值安全吗? Sequelize query() 能防止 SQL 注入吗? SQLAlchemy text() 能防止 SQL 注入吗? sqlx 编译时查询检查能防止 SQL 注入吗?