mysql拼接表名不能用占位符,因其只认数据值,对表名等语法结构无效;必须用严格白名单校验,且校验须在concat前执行,java层也需同步校验。

MySQL里拼接表名为什么不能用?占位符
因为?只认“数据值”,对表名、列名、ORDER BY字段、LIMIT偏移量这些语法结构完全无效。强行写SELECT * FROM ?会直接报错ERROR 1064 (42000)——这不是你SQL写得不够熟,是MySQL语法层面就不支持。
白名单校验必须硬编码,不能靠正则或模糊匹配
表名属于标识符,必须走严格白名单。常见错误是用LIKE或REGEXP做宽松校验,比如in_table REGEXP '^[a-z_]+$',这仍可能绕过(如传入users; DROP TABLE logs)。
- ✅ 正确做法:显式枚举可接受的表名,用
IF ... THEN ... END IF硬判断 - ❌ 错误做法:仅校验字符集、长度,或用
INSTR()过滤关键词 - 白名单应放在存储过程开头,校验失败立即
SIGNAL SQLSTATE '45000'中断执行
CONCAT拼接表名时,必须先校验再拼,顺序不能颠倒
哪怕白名单逻辑写对了,如果把CONCAT写在IF之前,攻击者仍能在校验前触发动态SQL执行。MySQL不会“等你校验完再执行”,它按语句顺序逐行解析。
- ✅ 正确顺序:
IF in_table NOT IN ('users', 'orders') THEN SIGNAL ...; END IF;→SET @sql = CONCAT('SELECT * FROM ', in_table); - ❌ 危险顺序:
SET @sql = CONCAT('SELECT * FROM ', in_table);→IF ... THEN SIGNAL ...;(此时SQL已构造完成,校验毫无意义) - 所有用户可控的标识符(表名、列名、排序字段)都需独立白名单,不能共用一个校验逻辑
Java侧调用时,别把白名单校验甩给数据库
很多人以为“我在存储过程里做了白名单,Java随便传就行”,这是错觉。Java层仍需做一次校验,理由有两个:
- 存储过程出错返回的是
SQLSTATE,Java捕获后处理成本高,不如提前拦截 - 白名单若随业务变化(比如新增
reports表),Java和存储过程必须同步更新,漏掉任意一端就等于开后门 - 建议在Service层用
Set.of("users", "orders", "logs")硬编码校验,而非读配置——配置易被篡改,且加载延迟不可控
白名单不是加个IF就万事大吉,它必须覆盖所有动态标识符入口、与调用方保持强一致性、且校验动作必须发生在任何CONCAT之前。漏掉任一环节,拼接表名就等于裸奔。











