预处理语句本身不防注入,真正起作用的是参数绑定机制;只有sql模板完全固定、用户输入全经?占位符+execute using绑定,mysql才能在服务端彻底隔离结构与数据。

预处理语句(PREPARE)本身不防注入,真正起作用的是参数绑定机制——只有所有用户输入都走 ? 占位符 + EXECUTE ... USING 绑定,MySQL 才会在服务端彻底隔离 SQL 结构与数据。
为什么直接用 PREPARE + 字符串拼接仍是高危操作
很多人误以为只要用了 PREPARE 就安全,结果写出类似这样的代码:
SET @sql = CONCAT("SELECT * FROM users WHERE name = '", $_GET['name'], "'");
PREPARE stmt FROM @sql;
EXECUTE stmt;
这和裸拼接没区别:恶意输入 admin' OR '1'='1 会直接进入 SQL 解析器。MySQL 的 PREPARE 只对静态模板生效;一旦变量提前拼进字符串,预编译就失去意义。
-
PREPARE防护的前提是 SQL 模板中不含任何用户输入,只含? - 动态拼接的字符串(哪怕加了引号或转义)都会绕过防护
- MySQL 不会对
@sql变量内容做二次解析或校验,它只把整个字符串当原始 SQL 处理
PREPARE + EXECUTE ... USING 的正确写法
必须严格遵循“模板固定、参数分离”原则。例如根据状态和时间范围查用户:
SET @sql = "SELECT id, name FROM users WHERE status = ? AND created_at > ?"; PREPARE stmt FROM @sql; SET @status = 1; SET @ts = '2025-01-01 00:00:00'; EXECUTE stmt USING @status, @ts; DEALLOCATE PREPARE stmt;
关键点:
-
?占位符不能加引号,也不能出现在引号内(如"WHERE name = '?' "是错的) -
USING后只能跟用户变量(@var),不能是字面量或表达式(USING 1, NOW()无效) - 变量赋值必须在
EXECUTE前完成,且类型要匹配字段(整数别传字符串变量) - 每次
PREPARE后建议DEALLOCATE PREPARE,避免连接级资源泄漏
哪些地方 PREPARE 根本无法保护,必须白名单校验
PREPARE 只能绑定值(WHERE 条件、VALUES、SET 赋值等),对语法结构完全无能为力:
-
ORDER BY ?或GROUP BY ?:MySQL 直接报错,字段名必须硬编码或从白名单映射(如IF @sort = 'created_at', 'created_at', 'id') -
SELECT * FROM ?:表名不支持占位,需正则过滤(/^[a-z_][a-z0-9_]{1,63}$/i)或查配置中心白名单 -
LIMIT ?, ?中第一个参数(offset):必须先转成整数并校验 ≥ 0,否则可能触发隐式类型转换漏洞 -
LIKE ?:通配符(%、_)必须由业务逻辑加到参数值里,不能拼进 SQL 模板
PHP/Python 等应用层调用时容易忽略的坑
应用层驱动(如 PHP 的 mysqli、Python 的 pymysql)底层其实也走 MySQL 的 PREPARE 协议,但封装后容易掩盖问题:
- PHP 的 PDO 默认开启模拟预处理(
PDO::ATTR_EMULATE_PREPARES = true),等于在 PHP 层自己拼 SQL,必须显式关掉:$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false) - Python 的
pymysql只认%s占位符,写成'%s'或用.format()就退化为拼接 - MyBatis 中
${}是字符串替换,和PREPARE无关;只有#{}才触发真正的参数绑定 - 所有语言中,
IN (?, ?, ?)的占位符个数必须由代码逻辑确定,不能靠用户输入动态生成字符串(如拼str_repeat('?,', $n))
最常被忽略的一点:预处理只解决“数据怎么进”,不解决“结构怎么定”。表名、字段名、排序逻辑、权限控制、错误信息泄露——这些环节出问题,再严格的 PREPARE 也救不了。











