${}是字符串直接替换,不经过预编译,用户输入可篡改SQL结构;动态表名必须用${}时,须在Service层用白名单(如Enum或Set)严格校验,或用<choose>标签枚举静态表名,严禁在@Select注解中拼接${tableName}。

动态表名无法用 #{} ,必须用 ${} ,但直接拼接就等于把数据库的钥匙交给用户——只要输入 user; DROP TABLE user--,就能删库跑路。
为什么 ${tableName} 会触发 SQL 注入
MyBatis 的 ${} 是纯文本替换,不经过 PreparedStatement 预编译。数据库收到的 SQL 就是拼好的完整字符串,所有字符都参与语法解析。攻击者只要让输入突破“表名”语义边界,就能插入额外语句。
- 输入
user; DELETE FROM config--→ 实际执行:SELECT * FROM user; DELETE FROM config-- WHERE id = ? - 输入
user WHERE 1=1 UNION SELECT password FROM admin→ 表名字段被当成了 WHERE 子句的一部分 - 即使加了引号(如
'${tableName}'),MySQL 也不允许表名加单引号,反而导致语法错误或绕过过滤
白名单校验:最直接、最可靠的防御方式
只允许传入预定义的合法表名,其他一概拒绝。不是“过滤危险词”,而是“只放行已知安全项”。
- 在 Service 层或 Controller 层做判断,不要依赖前端或参数校验注解(它们可被绕过)
- 用
Set<String>或Enum管理白名单,避免硬编码散落在各处 - 校验失败时抛出
IllegalArgumentException,不返回模糊提示(如“表名非法”可能暴露存在哪些表) - 示例逻辑:
if (!ALLOWED_TABLES.contains(tableName)) { throw new IllegalArgumentException("Invalid table name: " + tableName); }
用 <choose> 替代 ${}:XML 中更安全的写法
当表名选项有限(比如按月分表 log_202601、log_202602),优先用 MyBatis 动态标签显式枚举,彻底规避字符串拼接。
-
<choose>内部仍是静态 SQL 片段,不会被用户输入影响结构 - 每个
<when test="...">分支对应一个确定的表名,没有运行时拼接 - 示例:
<select id="queryByMonth" resultType="Log"> SELECT * FROM <choose> <when test="month == '202601'">log_202601</when> <when test="month == '202602'">log_202602</when> <otherwise>log_default</otherwise> </choose> WHERE level = #{level} </select>
@Select 注解里动态表名怎么防注入
@Select 不支持 <choose>,也不能在注解里写 Java 逻辑,所以**禁止在 @Select 中直接用 ${tableName}**。
- 改用 XML 映射文件,把动态逻辑移出去——这是最稳妥的选择
- 如果必须用注解,先在方法内完成白名单校验,再调用一个固定 SQL 的
@Select(例如@Select("SELECT * FROM log_202601 WHERE ...")) - 绝对不要写类似
@Select("SELECT * FROM ${tableName} WHERE ..."),IDE 可能不报错,但上线就是高危漏洞
真正难的不是写白名单,而是想清楚:这个“动态表名”需求是否真实必要?很多场景其实是设计缺陷——比如本该用分区表、本该加业务字段区分类型、本该走配置中心而非硬编码表名。一旦接受“必须动态”,就得守住白名单这道门,而且得守在 DAO 层之前,不能靠信任任何下游输入。

















