参数化查询能防止SQL注入是因为数据库驱动将SQL结构与用户输入分离处理,前者编译执行,后者仅作绑定变量的纯数据;但表名、字段名等结构部分无法参数化,须白名单校验,且ORM的raw query仍需警惕。

参数化查询为什么能拦住大部分 SQL 注入
因为数据库驱动会把 query 和 params 分开处理:SQL 结构走编译路径,用户输入只当纯数据塞进绑定变量,根本不会被解析成语法的一部分。哪怕传入 ' OR 1=1 --,它也只是个字符串值,不是 SQL 拼接后的新语句。
但前提是——你得真正在用参数化,而不是“看起来像”:
- Python 的
sqlite3.execute("SELECT * FROM users WHERE id = ?", (user_id,))✅ - PHP 的
$stmt = $pdo->prepare("SELECT * FROM users WHERE name = ?"); $stmt->execute([$name]);✅ - Java 的
PreparedStatement配合setString()✅ - 错例:
"SELECT * FROM users WHERE id = " + user_id❌(拼接即破防) - 错例:
cursor.execute(f"SELECT * FROM users WHERE name = '{name}'")❌(f-string 或.format()都不安全)
哪些地方不能靠参数化,必须额外校验
参数化只适用于「值」,不适用于「结构」。表名、字段名、排序方向(ASC/DESC)、LIMIT 偏移量这些,数据库驱动根本不允许参数化。
比如你想动态选表:SELECT * FROM ? 是语法错误;想控制排序:ORDER BY name ? 也会报错。
这时候只能白名单校验:
- 表名映射到预设字典:
table_map = {"user": "users", "order": "orders"},只接受 key 存在才拼接 - 排序字段限定为
["id", "created_at", "status"],用if sort_field not in allowed_fields:拦住 -
LIMIT数值必须是正整数且有上限(比如min(int(limit), 100)),避免LIMIT 1000000拖垮查询 - 绝对不要对这类位置做正则替换或截断——
re.sub(r"[^a-zA-Z_]", "", user_input)仍可能绕过(比如双写绕过、编码混淆)
ORM 不等于自动免疫,尤其在 raw query 和表达式里
Django、SQLAlchemy 这类 ORM 默认用参数化,但一旦你调用 raw()、text() 或拼接 filter(Q(...)) 表达式,防线就松动了。
常见翻车点:
- Django:
User.objects.extra(where=["name = '%s'" % name])❌(%格式化直接裸奔) - SQLAlchemy:
session.execute(text("SELECT * FROM users WHERE role = :role"), {"role": role})✅,但写成text(f"SELECT ... WHERE role = '{role}'")❌ - Sequelize:
Model.findAll({ where: { [Op.or]: [{ email: req.query.q }] } })安全;但sequelize.query(`SELECT * FROM users WHERE ${req.query.sort}`)❌ - 所有 ORM 的
.raw()、.execute()接口,只要含字符串拼接,就等同于手写 SQL
为什么加一层类型/长度/格式校验不是多余
参数化解决“是不是 SQL”,校验解决“合不合理”。两者漏一不可。
比如一个手机号字段,即使用了参数化,传入超长字符串(如 10MB 的 aaaa...a)可能触发日志爆炸、内存溢出,或为后续逻辑埋雷(比如下游服务假设手机号是 11 位)。
实用建议:
- 对数字型 ID,用
int()强转并捕获ValueError,别信isnumeric() - 邮箱用
email-validator库(非正则)校验格式和 DNS 可达性,比前端 JS 校验可靠得多 - 用户名限制长度(如 ≤32 字符)、字符集(
^[a-zA-Z0-9_\-\.]+$),拒绝 emoji 和零宽空格 - 校验失败时统一返回 400,不暴露字段名或规则细节(避免被反向探测)
最常被忽略的是:校验必须在参数化之前做。否则无效输入已进入数据库驱动栈,可能触发异常路径里的意外行为。










