foreach标签本身不防SQL注入,安全完全依赖循环体内使用#{item}而非${item};配错collection或item导致BindingException,空集合必须用<if>兜底否则SQL语法断裂。

foreach 标签本身不防 SQL 注入,安全完全依赖于你循环体内用的是 #{item} 还是 ${item}。
必须用 #{item},禁用 ${item} 在 foreach 内部
foreach 只是语法糖,负责拼出 ( ?, ?, ? ) 结构;它不校验、不转义、不拦截任何输入。真正起防护作用的是 #{id} 触发的 PreparedStatement 预编译机制。
- ✅ 安全写法:
<foreach collection="ids" item="id">#{id}</foreach>→ 每个值都走预编译,数据库只当数据处理 - ❌ 危险翻车:
<foreach collection="ids" item="id">#{id} OR name = ${name}</foreach>→ 一处 ${name} 就让整条 SQL 失去防护 - ❌ 模糊搜索也一样:
CONCAT('%', #{kw}, '%')安全;CONCAT('%', ${kw}, '%')高危,哪怕 kw 来自 foreach 循环内 - ❌ 动态字段名(如
ORDER BY ${sortField})和 foreach 无关,必须用白名单校验,不能指望 foreach 拦住
collection 和 item 必须严格匹配参数类型与命名
配错不会导致注入,但会抛 BindingException 或生成空 SQL,属于运行时错误,容易漏测。
- 接口方法是
UserMapper.selectByIds(@Param("userIds") List<Long> ids)→ XML 中collection="userIds",不是"list"或"ids" - 传的是
Long[]数组且没加@Param→collection="array",不是"list" -
item="uid",但循环体里写#{userId}→ OGNL 找不到变量,报错 - 遍历
List<String>却写#{uid.name}→ 运行时报ognl.MethodFailedException
空集合或 null 必须用 <if> 显式兜底
foreach 遇到 null 或空集合时默认不输出任何内容,会导致 SQL 语法断裂,比如 WHERE id IN 后直接接 AND status = 1,抛 SQLSyntaxErrorException。
- ✅ 正确兜底:
<if test="ids != null and !ids.isEmpty()">id IN <foreach ...></foreach></if> - ❌ 别用
ids.size() > 0替代!ids.isEmpty(),某些 MyBatis 版本对空集合调用size()可能 NPE - ❌
<where>标签能自动 trim 首段 AND,但它不判断集合是否为空,不能替代<if> - ❌
IN #{ids}直接传集合?JDBC 不支持,MyBatis 会报错或静默失败
最常被忽略的是空集合兜底逻辑——它不报 BindingException,日志里也看不出异常,但查询直接崩。别假设“业务上不会传空”,要像处理 null 一样写防御性代码。

















