参数化查询是防 SQL 注入的强制边界,因用户输入混入 SQL 字符串会导致解析失控;动态表名 / 字段名等无法参数化,须用白名单校验。

为什么 raw SQL 拼接是注入温床
因为用户输入直接混进 SQL 字符串里,数据库分不清哪部分是逻辑、哪部分是数据。WHERE name = '+ user_input +' 这种写法,只要 user_input 是 ' OR 1=1 --,整条查询就失控了。参数化查询不是“可选优化”,是执行边界——数据必须走参数通道,绝不能走字符串拼接通道。
用 sqlx(Rust)或 pg(Node.js)做参数化查询
这类库强制你把值单独传入,底层用数据库原生的 prepared statement 机制,值永远不会被解析为 SQL 语法。
-
sqlx::query("SELECT * FROM users WHERE id = $1").bind(user_id)——$1是占位符,bind()传的值不参与 SQL 解析 -
pool.query("SELECT * FROM users WHERE email = $1", [email])(PostgreSQL + Node.jspg)——数组里每个元素严格对应一个$N - 别用
format!或模板字符串拼 SQL;也别在WHERE里动态插表名 / 字段名——这些无法参数化,得靠白名单校验
ORM 的 .where() 方法真能防注入?
能,但只在它接管整个条件构建时才成立。比如 Prisma.user.findMany({where: { email: req.query.email} }) 是安全的;但一旦你写成 where: Prisma.sql`email = ${req.query.email}`,就退回拼接模式,防线崩溃。
- 检查 ORM 文档是否明确标注该方法“支持参数化”或“自动转义”
- 警惕
.raw()、.unsafe()、Prisma.sql这类显式绕过安全层的 API - 复杂动态查询(如多条件组合)优先用 ORM 的链式
.where()构建,而不是手拼 SQL 字符串
预编译语句失效的三个隐蔽时刻
即使用了参数化,某些操作仍会意外触发字符串拼接,导致防护失效。
- 动态表名或字段名:
SELECT * FROM ${table_name}—— 数据库不支持参数化标识符,必须用白名单比对,例如if !["users", "posts"].contains(&table) {panic!() } - ORDER BY 后的字段:
ORDER BY ${sort_field}—— 同样不能参数化,需限定为枚举值或正则校验(如^[a-zA-Z_][a-zA-Z0-9_]*$) - IN 子句多个值:
WHERE id IN ($1, $2, $3)需要提前确定参数个数;若数量不定,得用ANY($1)配合数组参数,而非拼出一长串$1,$2,……
最常被忽略的是动态结构部分——表名、字段名、排序方向、LIMIT 偏移量。它们看起来像“数据”,其实是 SQL 语法的一部分,参数化机制根本不覆盖。这里没有捷径,只有白名单或严格模式校验。