存储过程中拼接SQL字符串是高危行为,必须用白名单校验表名等标识符或INFORMATION_SCHEMA运行时验证,禁用用户输入进入危险语句,并限制账号权限与SQL SECURITY模式。

存储过程中用 CONCAT 或 + 拼接 SQL 字符串就是高危行为
只要你在 CONCAT、+、GROUP_CONCAT 或 REPLACE 里塞了用户输入,哪怕只拼一个表名或字段名,就等于把数据库的执行权交给了攻击者。MySQL 存储过程不支持对动态语句里的占位符做类型绑定,PREPARE + EXECUTE 只能参数化值,不能参数化标识符(如表名、列名、ORDER BY 字段)。所以这类拼接一旦失控,SELECT ... INTO OUTFILE、LOAD DATA INFILE、UNION SELECT 全都可能被带出来。
必须用白名单校验动态部分,不能靠过滤或转义
常见错误是写个 REPLACE(user_input, "'", "''") 或正则删掉分号——这些在 MySQL 存储过程里根本挡不住注入。真正有效的做法只有两个:
- 所有可变的标识符(如表名、列名、排序字段)必须从预定义白名单中匹配,比如用
CASE WHEN @table_name = 'users' THEN 'users' WHEN @table_name = 'orders' THEN 'orders' ELSE NULL END强制映射 - 用
INFORMATION_SCHEMA做运行时校验:先查SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = @table_name,结果为 0 就直接LEAVE - 禁止任何用户可控内容进入
INTO DUMPFILE、INTO OUTFILE、LOAD DATA等语句路径,哪怕加了白名单也得额外检查权限
EXECUTE IMMEDIATE 不等于安全,它只是语法糖
MySQL 8.0+ 支持 EXECUTE IMMEDIATE,但它的底层仍是 PREPARE + EXECUTE,同样无法参数化标识符。你写 EXECUTE IMMEDIATE CONCAT('SELECT * FROM ', @tbl),和老写法没区别。重点不是换语法,而是控制 @tbl 的来源——它必须来自硬编码字符串、常量变量或白名单校验后的结果,绝不能是直接传入的参数。
权限最小化比代码防御更关键
即使存储过程逻辑再严谨,如果调用账号有 FILE、SUPER、PROCESS 权限,攻击者仍可能通过注入触发提权链。务必确认:
- 调用该存储过程的账号已执行
REVOKE FILE ON *.* FROM 'app_user'@'%',并用SHOW GRANTS验证 -
secure_file_priv已设为受限目录(如/var/lib/mysql-files/),且local_infile为OFF - 业务账号无
EXECUTE权限访问系统函数(如sys_exec),若有 UDF 加载记录需立即DROP FUNCTION
最常被忽略的一点:存储过程本身是“以定义者权限执行”的,默认会继承 DEFINER 账号的全部权限。哪怕调用者只是普通账号,只要存储过程定义者是 root@localhost,整个注入链条就等同于 root 执行。必须显式改成低权限账号定义,或改用 SQL SECURITY INVOKER。

















