foreach标签本身不防注入,真正起作用的是#{item}的预编译;它仅生成(?, ?, ?)结构,安全依赖PreparedStatement对每个#{item}的绑定,${item}仍可导致SQL注入。

foreach 标签本身不防注入,真正起作用的是 #{item} 的预编译
很多人误以为 foreach 标签自带防注入能力,其实它只是个“循环模板生成器”:它负责把集合展开成 (?, ?, ?) 这样的占位符结构,但**真正的安全防线仍是每个 #{item} 被解析为 ? 并走 PreparedStatement 绑定**。如果写成 ${item},照样注入。
常见错误现象:
- XML 中写
<foreach collection="ids" open="(" separator="," close=")">${id}</foreach>→ 拼出IN (1, 2, DROP TABLE users) - Java 传入恶意字符串如
"1, 2; DROP TABLE user",再用${}直接拼进 SQL → 立刻执行破坏语句
正确用法要点:
-
collection属性必须准确(list/array/map key),否则 MyBatis 找不到集合,抛BindingException - 循环体内必须用
#{item}(或#{param}),不能用${item} - 若传入的是单个
List<long></long>,collection="list"是默认值,可省略;但显式写出更清晰
为什么 in (#{id}) 会报错而 foreach 不会
因为 JDBC 的 PreparedStatement **不支持对 IN 子句的单个占位符做集合绑定**——in (?) 只能匹配一个值,无法扩展为 in (?, ?, ?)。MyBatis 的 foreach 是在 SQL 解析阶段就动态生成对应数量的 ?,再由 JDBC 分别绑定每个值。
典型对比:
- ❌ 错误:
WHERE id IN (#{ids})→ MyBatis 尝试把整个 List 当作一个参数传给setObject(1, list)→ 报错或逻辑异常 - ✅ 正确:
WHERE id IN <foreach collection="ids" item="id" open="(" separator="," close=")">#{id}</foreach>→ 生成WHERE id IN (?, ?, ?),并调用三次setLong()
order by 和表名场景下,foreach 无能为力
foreach 只解决「值列表」问题,对「SQL 结构片段」(如字段名、表名、排序方向)完全无效。这些地方无法用 #{},因为 PreparedStatement 明确禁止对标识符做参数化。
所以你必须自己控制输入来源:
- 排序字段:Java 层定义白名单枚举或字符串数组,只允许传入索引或固定键名,再映射到真实字段名
- 动态表名:绝对不用
${tableName}接用户输入;改用配置驱动或服务路由,避免运行时拼接 - 模糊查询:
LIKE CONCAT('%', #{keyword}, '%')是安全的;LIKE '%${keyword}%'是高危的
最容易被忽略的细节:空集合和 null 处理
foreach 遇到空集合或 null 时,默认什么也不生成,可能导致 SQL 语法错误。例如:
-
WHERE id IN <foreach ...>#{id}</foreach>+ 空集合 → 最终 SQL 变成WHERE id IN→ 数据库报错
解决方案:
- 加
<choose><when test="ids != null and !ids.isEmpty()">...</when><otherwise>AND 1=0</otherwise></choose> - 或在 Java 层提前校验,空集合时直接返回空结果,不进 DAO
- 注意:OGNL 中判断 List 是否为空,必须用
!ids.isEmpty(),不能只写ids != null
真正的防护不在标签本身,而在你是否坚持让每一个用户可控的「值」都经过 #{} 走预编译——哪怕它在 foreach 里绕了一圈。

















