PREPARE语句必须通过@sql用户变量中转,即SET @sql=...→PREPARE stmt FROM @sql→EXECUTE stmt三步缺一不可;表名列名需白名单校验防注入,USING仅支持已赋值用户变量传参,且须DEALLOCATE释放资源。

PREPARE 语句不能直接执行拼接字符串,必须走 @sql 用户变量 + PREPARE stmt FROM @sql + EXECUTE stmt 三步;漏掉任何一步都会报错或注入。
PREPARE 只认用户变量,不接受 CONCAT() 直接入参
MySQL 的 PREPARE 语法强制要求源 SQL 必须来自一个已赋值的会话级用户变量(即以 @ 开头),比如 @sql。你不能写 PREPARE stmt FROM CONCAT('SELECT * FROM ', @table_name)——这会立刻触发 ERROR 1064 或 ERROR 1295。
- 必须先用
SET @sql = CONCAT(...)把完整 SQL 字符串存进用户变量 - 再调用
PREPARE stmt FROM @sql,否则语法不通过 - 局部变量(
DECLARE v_sql VARCHAR(1000))在PREPARE中完全不可用,声明了也报错 - 拼接时注意空格、引号闭合:比如
CONCAT('SELECT * FROM ', @tbl, ' WHERE id = ', @id)缺空格会导致FROMusers这类语法错误
表名/列名必须拼接,但必须严格校验防注入
占位符 ? 只能用于值(WHERE 条件、INSERT VALUES、LIMIT 数等),不能用于标识符。所以动态表名、列名、数据库名只能靠字符串拼接,而这是 SQL 注入高危区。
- 用正则校验输入:
IF @table_name NOT REGEXP '^[a-zA-Z_][a-zA-Z0-9_]*$' THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Invalid table name'; END IF; - 禁止用
TRIM()、REPLACE()“清洗”输入——my_table` --这类绕过依然有效 - 白名单更稳妥:把允许的表名存在临时表或 CASE WHEN 列表里匹配
- 不要拼接用户原始输入,而是映射成内部代号,比如传
'user_log_2026'→ 实际用CONCAT('log_', YEAR(NOW()))
EXECUTE USING 只支持值参数,且变量必须提前赋值
用 USING 传参比拼接安全得多,但它有硬性限制:只支持值替换,且所有 @var 必须是已声明并赋值的用户变量,不能是 NULL 字面量或函数调用结果。
-
EXECUTE stmt USING @val1, @val2;中的@val1和@val2必须在EXECUTE前已SET过,否则报ERROR 1210 - 不能写
EXECUTE stmt USING @id + 1或EXECUTE stmt USING NOW(),会语法报错 - NULL 要显式赋值:
SET @val = NULL;,不能直接USING NULL - 参数个数、顺序、类型必须和 SQL 中
?一一对应,多一个少一个都失败
DEALLOCATE PREPARE 不是可选操作,而是资源管理刚需
每个 PREPARE 都占用会话级句柄,不释放会在长连接中累积,最终触发 ERROR 1437(Out of memory; check if you need to increase the max_prepared_stmt_count value)。
- 每次
EXECUTE后应紧跟DEALLOCATE PREPARE stmt; - 同一个
stmt名重复PREPARE会自动覆盖前一个,但旧句柄仍驻留内存,直到显式释放或会话断开 - 若需复用(如循环中多次执行不同 SQL),建议用动态命名:
SET @stmt_name = CONCAT('stmt_', UNIX_TIMESTAMP()); PREPARE @stmt_name FROM @sql; - 存储过程退出时未释放,下次调用可能因句柄冲突失败,尤其在高并发场景下极易暴露
最常被忽略的是:校验表名列名时只检查字母数字,却忘了 MySQL 标识符允许反引号包裹含特殊字符的名称;而一旦放开反引号,`users`; DROP TABLE x# 就又成了注入入口——所以真正安全的做法,是拒绝一切非标准命名,坚持用纯字母+下划线白名单。











