MyBatis动态表名必须用${},但必须前置白名单校验,禁用配置/数据库读取的可篡改白名单,仅允许硬编码枚举或严格正则(如^[a-z][a-z0-9_]{2,31}$)校验,失败立即抛IllegalArgumentException。

MyBatis 动态表名必须用 ${}?那得先拦住它
MyBatis 里 ${tableName} 确实是唯一能拼表名的方式,但“必须用”不等于“可以随便用”。只要没做校验,${tableName} 就是 SQL 注入的直通车。关键不是换写法,而是把校验卡在字符串进入 SQL 之前——不是进 DAO 层之后,更不是进数据库之后。
白名单必须硬编码,不能从配置或数据库读
很多人把白名单存在配置中心或数据库里,以为“我管着呢”,结果攻击者一拿到低权限后台入口(比如弱口令、未授权接口),就能改掉白名单本身。校验逻辑一旦可被篡改,就等于没校验。
-
List.of("user_logs_v2024", "order_events_2025", "pay_records_q2")这种字面量枚举才是安全起点 - 正则必须严格:
^[a-z][a-z0-9_]{2,31}$(小写字母开头、3–32 字符、仅字母数字下划线) - 校验失败立即抛
IllegalArgumentException,绝不让非法值流到String.format("SELECT * FROM %s", safeName)这一步
MyBatis XML 中的 ${} 不是“动态”的借口
很多项目里 ${@table_prefix}user 这类写法被当成“固定拼接”,实际仍是 ${},且 prefix 可能来自配置表。grep 一下全项目:grep -r '\$\{.*\}' src/main/java/ --include="*.xml",把所有 ${} 拎出来逐个判断:
- 是否真需要动态?比如
${@shard_suffix}是分库分表必需,但${config.log_table}很可能只是历史包袱 - 是否已加白名单校验?没加就等同于裸奔
- 是否混在
<script>块里?那里 ${} 和 #{ } 容易混淆,必须一行行过
升级 MyBatis 版本不会自动修复 ${} 漏洞
从 3.4.6 升到 3.5.13,框架自身漏洞可能被堵住,但你代码里写的 SELECT * FROM ${tableName} 还是一样执行。升级反而可能暴露问题:新版本对 @SelectProvider 返回类型更严,或禁用 auto-mapping,导致原来“侥幸跑通”的错漏直接报错。
真正要动的是代码本身——所有 ${} 都得配上校验,所有动态结构参数都得走白名单兜底。最危险的不是不知道怎么修,而是以为“用了 MyBatis 就安全了”,或者“升级完就万事大吉”。

















