所有 ${} 必须先过硬编码白名单校验,禁止依赖“看起来安全”;mybatis 的 ${} 是纯字符串替换,易导致sql注入,需严格正则校验(如 ^a-z{2,31}$)、枚举约束 order by/表名动态化,并禁用 ${condition} 等高危用法。

所有 ${} 都得先过白名单校验,不能靠“看起来安全”蒙混
MyBatis 的 ${} 是纯字符串替换,数据库收到的就是拼好的完整 SQL。哪怕你写的是 ${@table_prefix}user,只要 @table_prefix 来自配置中心或数据库字段,攻击者改掉它就能注入。真实案例里,有人把白名单存在 MySQL 里,结果低权限后台接口被利用,直接篡改了白名单本身。
必须做到:
- 白名单硬编码在 Java 类里,比如
List.of("order_2024", "order_2025") - 正则校验必须严格:
^[a-z][a-z0-9_]{2,31}$(小写字母开头、3–32 字符、仅字母数字下划线) - 校验失败立刻抛
IllegalArgumentException,不传入 DAO 层一步 - grep 全项目:
grep -r '\$\{.*\}' src/main/java/ --include="*.xml",逐个确认每个${}是否真有必要、是否已加校验
ORDER BY 和表名动态化是 ${} 最常踩坑的两个场景
ORDER BY ${sortField} 和 FROM ${tableName} 是仅有的几个允许用 ${} 的地方,但也是最易被绕过的入口。前端传 sortField=user_id; DROP TABLE log;,XML 里没校验就直接执行。
安全做法不是“前端传字段名”,而是:
- Java 层定义枚举:
public enum SortField { id, name, created_at } - Controller 接收整型或字符串索引,映射到枚举值,再传给 MyBatis
- XML 中仍用
${sortField},但此时sortField已是可信枚举的 name() 或数据库字段名字面量 - 禁止任何形式的
${condition}或${sqlFragment}—— 这类写法等于主动交出 SQL 控制权
LIKE 和 IN 子句误用 ${} 是新手高频雷区
模糊查询写成 WHERE name LIKE '%${keyword}%',IN 查询写成 id IN (${ids}),表面看只是语法报错,实际是把注入大门焊死了。这类问题根本不需要 ${}。
正确姿势:
- LIKE:用
CONCAT('%', #{keyword}, '%')(MySQL)或#{keyword} || '%'(PostgreSQL),保持预编译 - IN:用
<foreach></foreach>标签:id IN <foreach collection="ids" item="id" open="(" separator="," close=")">#{id}</foreach> - 别碰
${keyword}做模糊匹配——99% 的场景都能用#{}加数据库函数兜住
拦截器不能替代白名单,但能帮你发现漏网的 ${}
写一个 SqlInjectionInterceptor 拦截 StatementHandler.prepare,用正则扫 UNION|SELECT|DROP 等关键字,确实能在测试环境快速暴露问题。但它只是“报警器”,不是“防火墙”。
注意点:
- 拦截器无法识别合法业务语句里的关键字(比如表名含
user_log) - 绕过手段太多:双写
SELESELECTCT、十六进制编码、注释分隔等 - 真正该花时间的地方,是把所有
${}替换为#{},或补上硬编码白名单校验 - 拦截器更适合做上线前扫描和灰度期告警,而非生产环境兜底
最麻烦的从来不是写校验逻辑,而是找到所有藏在 <script></script> 块里、@SelectProvider 返回值中、甚至 generator 自动生成 XML 里的 ${} —— 它们往往分散在不同模块,没人记得当初为什么这么写。查一遍,改一遍,卡死校验,比等渗透报告来得实在。










