MySQL预编译防注入必须用USING传参,?仅适用数据值;动态标识符需硬编码白名单校验;PREPARE需显式释放;MyBatis中${}仍存在注入风险。

MySQL中PREPARE/EXECUTE必须用USING传参,不能拼接变量
直接字符串拼接@sql再EXECUTE是高危操作,哪怕参数来自内部系统。MySQL的PREPARE本身不防注入,它只保证语法合法;真正起防护作用的是USING子句——它把值作为独立参数传递,交由MySQL驱动做二进制绑定。
常见错误现象:SET @sql = CONCAT('SELECT * FROM users WHERE id IN (', @ids, ')'); 然后EXECUTE stmt——此时@ids若为'1\', \'2\' OR 1=1',就会触发注入。
正确做法必须走USING:
SET @sql = 'SELECT * FROM users WHERE id = ?'; PREPARE stmt FROM @sql; EXECUTE stmt USING @user_id;
要点:
-
?占位符只能用于数据值,不能用于表名、列名、ORDER BY字段等标识符 -
USING后只能跟用户变量(@var),不能跟字面量或表达式 - 每个
?对应一个@var,数量必须严格一致
动态表名/列名必须白名单校验,不可绕过
当业务真需要动态表名(如分表查询)或排序字段(如ORDER BY ${sortField}),USING完全失效——因为?不能替代标识符。这时唯一安全路径是:在拼接前做硬编码白名单校验。
示例(存储过程中):
IF sort_field NOT IN ('created_at', 'updated_at', 'status') THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Invalid sort field';
END IF;
SET @sql = CONCAT('SELECT * FROM orders ORDER BY ', sort_field);关键限制:
- 白名单必须写死在SQL里,不能从配置表或参数读取——否则白名单本身又被注入
- 校验逻辑必须在
CONCAT之前,且用NOT IN或CASE WHEN,避免用LIKE或正则引入绕过可能 - MySQL 8.0+ 可用
JSON_CONTAINS做更灵活的白名单,但依然要确保JSON内容不可控
PREPARE语句生命周期管理容易被忽略
PREPARE生成的语句句柄(stmt_name)是会话级资源,不显式释放会持续占用内存,尤其在长连接或连接池场景下易引发泄漏。
典型问题:
- 循环中反复
PREPARE stmt FROM @sql却不DEALLOCATE PREPARE stmt,导致“Out of memory”错误 - 存储过程异常退出时未清理,句柄残留直到会话断开
- 同名
stmt重复PREPARE会覆盖旧句柄,但旧资源未必立即释放
建议写法:
BEGIN
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
DEALLOCATE PREPARE IF EXISTS stmt;
RESIGNAL;
END;
<p>PREPARE stmt FROM @sql;
EXECUTE stmt USING @param;
DEALLOCATE PREPARE stmt;
ENDMyBatis等ORM中#{}和${}混用时的陷阱
很多人以为“用了MyBatis就安全了”,其实只要混用${},风险立刻回归。比如ORDER BY ${sortField} + WHERE status = #{status},后者安全,前者直接暴露。
真实踩坑点:
-
${}里的变量若来自HTTP参数或数据库字段(如用户偏好设置),等于把SQL控制权交给了外部 - 即使加了Java层校验,也挡不住绕过接口直调Mapper XML的攻击路径
- MyBatis日志默认不打印
${}替换后的完整SQL,出问题时难定位
替代方案优先级:
- 能用
#{}的地方绝不用${} - 必须动态标识符时,改用
<bind></bind>配合白名单校验,或前置服务层转换为固定枚举 - 金融/政务类项目,CI阶段应强制扫描XML中所有
${}并人工确认
最常被忽略的一点:预编译的性能优势只对#{}生效。${}每次生成新SQL字符串,MySQL无法复用执行计划——高并发下CPU飙升往往就源于此。


















