mybatis动态sql标签(如、、)本身不防注入,真正起防护作用的是标签内使用的#{}占位符;只要参数绑定统一采用#{},即使嵌套多层动态逻辑也安全,而${}拼接(如in (${ids})或like '%${username}%')则存在高危sql注入风险。

动态SQL标签本身不防注入,但默认用#{}就安全
MyBatis的<if></if>、<where></where>、<foreach></foreach>这些标签只是语法糖,它们不自动“消毒”参数——真正起防护作用的是你写在里面的占位符。只要内部用#{},哪怕条件层层嵌套,也不会引入注入风险。
常见错误是看到<if test="name != null"></if>就以为“写了动态SQL就等于安全了”,结果里面写成AND name = ${name},等于白搭。
-
<if></if>里的表达式(如name != null)运行在Java侧,不进SQL,无需防注入;但AND name = #{name}这句才进SQL,必须用#{} -
<where></where>只负责智能拼接WHERE和AND,不处理参数绑定逻辑 -
<foreach></foreach>的collection、item等属性是Java变量名,安全;但#{item}才是实际传入SQL的值,必须用#{}
in语句里传多个值,别手写${}拼接括号
写id IN (${ids})是典型高危操作:用户传"1, 2, 3; DROP TABLE users--"就能触发注入。MyBatis原生支持<foreach></foreach>生成安全的预编译参数列表。
正确写法示例:
<select id="selectByIds" resulttype="User">
SELECT * FROM user WHERE id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach></select>
- 生成的SQL是
WHERE id IN (?, ?, ?),数据库收到的是三个独立参数,不是字符串拼接 -
collection必须是List/Array/Set等可迭代对象,不能是逗号分隔的String - 如果硬要传字符串(如前端给
"1,2,3"),得先在Java层split(",")转成List再传,不能直接${ids}
like模糊查询时,通配符必须由Java或数据库函数包裹
username LIKE '%${username}%'是送命写法——攻击者输"a%' OR '1'='1"就破防。MyBatis不负责加%,你得主动控制拼接位置。
推荐两种安全做法:
- Java层拼好再传:
mapper.selectByUsernameLike("%" + userInput + "%"),XML里仍用#{username} - SQL层用函数:
username LIKE CONCAT('%', #{username}, '%')(MySQL)或username LIKE '%' || #{username} || '%'(PostgreSQL) - 绝对不要用
${}拼%,比如'%${username}%'或${'%'+username+'%'}——MyBatis不解析EL表达式,后者会直接报错或空值
order by和表名/列名动态替换,${}必须配白名单校验
ORDER BY ${sortColumn}或FROM ${tableName}这类场景无法用#{},因为预编译不支持动态结构。此时${}是唯一选择,但必须前置校验。
关键点不是“怎么写SQL”,而是“怎么拦输入”:
- 定义允许的列名集合:
private static final Set<string> ALLOWED_SORT_COLUMNS = Set.of("id", "name", "create_time");</string> - 校验逻辑必须在DAO调用前完成,不能放在XML里(XML没校验能力)
- 正则校验
^[a-zA-Z][a-zA-Z0-9_]*$比单纯检查长度更可靠,但不如白名单精准 - 注解方式(如
@Select("SELECT * FROM ${table}"))风险更高——校验逻辑更难统一管控
最易被忽略的是:白名单校验必须覆盖所有可能路径,包括接口参数、DTO字段、甚至缓存键里的动态片段——漏一个就等于开了后门。










