MySQL存储过程并非SQL注入免疫区,使用CONCAT()拼接用户输入或PREPARE+EXECUTE执行动态SQL仍会引发注入;安全做法是:值用EXECUTE ... USING绑定参数,表名/列名等语法元素须严格白名单校验。

MySQL存储过程中为什么还会存在SQL注入
存储过程本身不是“免疫区”——只要里面用了 CONCAT() 或 EXECUTE IMMEDIATE(在 MySQL 中是 PREPARE + EXECUTE)拼接用户输入,就可能被注入。常见错误是:把前端传进来的参数直接拼进动态 SQL 字符串里,再执行。
典型危险写法:
DELIMITER $$
CREATE PROCEDURE get_user_by_name(IN p_name VARCHAR(50))
BEGIN
SET @sql = CONCAT('SELECT * FROM users WHERE username = ''', p_name, '''');
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
END$$
DELIMITER ;攻击者调用 CALL get_user_by_name('admin'' OR ''1''=''1'); 就能绕过条件,查出所有用户。
用参数化方式重写动态 SQL(MySQL 8.0+ 支持)
MySQL 8.0.13 起,EXECUTE ... USING 支持绑定变量,这是唯一真正安全的动态 SQL 方式。必须把用户输入作为参数传入,而不是拼进字符串。
- 把
CONCAT()拼接整条 SQL 的逻辑全部删掉 - 只对固定结构部分使用字符串拼接(如表名、字段名),且这些值必须来自白名单或严格校验
- 所有用户可控内容(如查询条件值)一律通过
USING绑定
安全改写示例:
DELIMITER $$ CREATE PROCEDURE get_user_by_name(IN p_name VARCHAR(50)) BEGIN SET @sql = 'SELECT * FROM users WHERE username = ?'; PREPARE stmt FROM @sql; EXECUTE stmt USING p_name; -- ← 关键:这里传参,不是拼接 DEALLOCATE PREPARE stmt; END$$ DELIMITER ;
注意:? 占位符只能替代**值**,不能替代表名、列名、排序方向(ASC/DESC)等语法结构。这些若需动态,必须额外做白名单校验。
表名/列名等语法元素如何安全动态化
如果业务真需要动态表名(比如分表查询),? 无法处理,必须手动过滤。别信正则“过滤掉非法字符”,要直接比对白名单。
- 定义允许的表名数组(如
SET @allowed_tables = 'users,orders,logs';) - 用
FIND_IN_SET(p_table_name, @allowed_tables)判断是否合法 - 拒绝一切未明确列出的名称,连空格、点号、反引号都不放行
- 绝对不要用
REPLACE(p_name, '''', '')这类“清理”逻辑——绕过方法太多
错误示范:SET @table = CONCAT('`', p_table_name, '`'); —— 只要 p_table_name 是 users` UNION SELECT password FROM mysql.user -- ,照样崩。
旧版本 MySQL(
低于 8.0.13 的 MySQL 不支持 EXECUTE ... USING,意味着你根本没法在存储过程中安全地使用用户输入做动态查询。这时候唯一靠谱的选择是:
- 把动态逻辑移到应用层(PHP/Python/Go),用该语言的预处理机制(如 PDO 的
prepare()+execute())处理 - 存储过程只做纯逻辑封装,不碰用户输入拼接
- 如果硬要留在存储过程里,只能靠输入长度限制 + 白名单 + 应用层二次校验,但风险不可忽视
很多线上系统卡在 5.7,这类场景下,存储过程应退回到“只查固定表、固定字段”的静态模式,把可变部分交给上层控制。
最常被忽略的一点:即使用了 USING,如果存储过程内部还调用了其他拼接型子过程,或者把参数转给另一个危险的 PREPARE 语句,防线依然会塌。安全必须贯穿整条调用链。


















