mysql存储过程防注入关键在动态sql写法:数据值必须用prepare+execute+?占位符,表名列名等标识符须白名单校验后concat拼接,严禁直接拼接用户输入。

MySQL 存储过程本身不自动防注入,关键在怎么写——只要用 CONCAT 拼接用户输入,哪怕只拼一个单引号,就等于把数据库大门钥匙交出去。
动态SQL必须用 PREPARE + EXECUTE + ? 占位符
所有用户可控的数据值(比如 WHERE 条件、INSERT 的 VALUES、UPDATE 的 SET 值)都得走 ? 占位符,由 MySQL 服务端做类型绑定和边界隔离。
- ✅ 安全写法:
SET @sql = 'SELECT * FROM users WHERE status = ? AND created_at > ?'; PREPARE stmt FROM @sql; EXECUTE stmt USING @status_val, @time_val; - ❌ 危险写法:
SET @sql = CONCAT('SELECT * FROM users WHERE name = ''', in_name, '''');—— 攻击者传in_name = 'admin'' OR ''1''=''1'就直接绕过 - ⚠️ 注意:
USING后面只能跟用户变量(@xxx),不能直接写存储过程参数(如in_name),否则报错ERROR 1318 (42000) - ? 实操建议:先显式赋值
SET @name_param = in_name;,再EXECUTE stmt USING @name_param;
表名、列名、排序字段等标识符绝不能用 ?
? 在 MySQL 里只认“数据值”,对表名、列名、函数名、LIMIT 偏移量这些语法结构完全无效。强行用会直接报错 ERROR 1064 (42000)。
- ❌ 错误尝试:
SET @sql = 'SELECT * FROM ? WHERE id = ?';—— 第一个?不被支持 - ✅ 正确做法:白名单硬校验 +
CONCAT拼接,例如:IF in_table NOT IN ('users', 'orders', 'logs') THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Invalid table'; END IF; - ? 常见易忽略点:ORDER BY 字段、GROUP BY 列、SELECT 中的别名、甚至
LIMIT ?, 10中的第一个参数(偏移量)都属于标识符范畴,不能用?
用户变量生命周期和 NULL 处理陷阱
@xxx 变量跨 EXECUTE 有效,但作用域仅限当前连接,且类型隐式转换容易静默失败。
- ⚠️ 类型错配:
@id_param是字符串但目标字段是INT,MySQL 可能转成0,查不到数据也不报错 - ⚠️ NULL 问题:
USING @val传入NULL,生成的条件是col = NULL(永远为 FALSE),得单独判断IF in_val IS NULL THEN ... ELSE ... END IF; - ⚠️ 作用域混淆:不要依赖
@xxx在多次存储过程调用间保持值——退出后就销毁,下次调用是全新状态 - ? 推荐写法:
SET @val = IFNULL(in_input, '');或显式类型转换:SET @id_param = CAST(in_id AS UNSIGNED);
性能与权限常被忽略的硬伤
高频调用下,PREPARE 的解析开销比静态 SQL 高 3~5 倍;而权限没收紧,等于给攻击者留了后门。
- ⚡ 性能注意:每次调用都会重编译语句,避免在循环里反复
PREPARE/DEALLOCATE;高频场景优先考虑静态 SQL 或缓存预编译句柄 - ? 权限注意:调用账户不能有
SELECT表权限,只授EXECUTE存储过程权限;用DEFINER指定高权限上下文,而非靠调用者权限兜底 - ? 容易漏掉的点:
LOG表、INFORMATION_SCHEMA访问、临时表创建权限——这些都可能被用于探测或提权
真正难的不是写对一行 PREPARE,而是整套逻辑里每个拼接点、每个变量来源、每个标识符分支都得同步守住——漏一个,前面全白忙。











