MyBatis-Plus的QueryWrapper和LambdaQueryWrapper本身不防注入,安全取决于使用方式;eq、like等方法通过PreparedStatement参数绑定实现防注入;LambdaQueryWrapper通过方法引用避免字段名污染;apply和${ew.customSqlSegment}需手动防范SQL注入。

MyBatis-Plus 的 QueryWrapper 和 LambdaQueryWrapper 本身不防注入,安全完全取决于你传什么、怎么传——只要不用字符串拼接、不把用户输入当 SQL 结构用,内置 eq、like 等方法默认就是安全的。
为什么 eq、like 等方法默认不会导致 SQL 注入
它们底层全部走 JDBC PreparedStatement 参数绑定,和 MyBatis 的 #{} 本质一致:参数值只作为数据传入,不参与 SQL 解析。
-
queryWrapper.eq("username", "admin' OR '1'='1")生成的是WHERE username = ?,数据库只会去查用户名等于这个完整字符串的记录 -
queryWrapper.like("name", keyword)是安全的;但写成"%" + keyword + "%"就可能出问题——尤其当keyword含%或_且没配escape规则时,语义会被破坏 - 动态条件记得加布尔开关:
queryWrapper.eq(name != null, "name", name),避免空值误加条件
为什么 LambdaQueryWrapper 比 QueryWrapper 更可靠
关键不在 Wrapper 类型,而在字段引用方式:User::getUsername 在编译期就锁定了字段路径,运行时通过反射提取字段名,不经过字符串解析。
-
queryWrapper.eq("status", input)中的"status"是纯字符串,若来自前端或配置项,可能被篡改为"status; DROP TABLE user" -
lambdaWrapper.eq(User::getStatus, input)的字段来源是类结构,外部无法污染 - 排序字段同理:
lambdaWrapper.orderByAsc(User::getCreateTime)彻底避开字符串字段名风险
apply 是唯一能主动打开注入通道的地方
apply 不管你前面用了多少个 User::getXXX,只看你传进去的 SQL 片段干不干净。它不区分“结构”和“值”,必须手动控制。
- ❌ 危险:
wrapper.apply("status = '" + statusValue + "'")——statusValue是"1'; TRUNCATE user; --"就直接执行 - ✅ 安全:
wrapper.apply("status = {0}", statusValue)——{0}触发参数绑定,等价于? - ⚠️ 注意:
{0}只对值生效,不能用于字段名、表名、排序方向(如ORDER BY {0}会报错或被忽略) - 真要动态结构?必须白名单校验后映射到方法引用,例如:
if ("price".equals(sortField)) wrapper.orderByAsc(User::getPrice)
使用 @Select + ${ew.customSqlSegment} 时的隐藏风险
这种写法常见于需要自定义 SQL 主体但保留 Wrapper 动态条件的场景,但容易踩三个坑:
- SQL 注入风险:如果
customSqlSegment被恶意构造(比如前端传参未校验),${}会原样拼接,不走任何绑定 - 条件重复:自定义 SQL 中已含
WHERE status = 1,又在 Wrapper 中加eq("status", 2),最终变成WHERE status = 1 AND status = 2,结果为空 - 分页失效:MP 分页插件通常只拦截标准
selectPage调用,对@Select+${ew.customSqlSegment}不自动处理LIMIT和COUNT
最易被忽略的一点是:LambdaQueryWrapper 对 apply 和 ${ew.customSqlSegment} 完全不设防——它只管字段引用,不管你怎么塞 SQL 片段。一旦你写了 apply("ORDER BY " + sortField) 或在 XML 里用 ${sortField},前面所有类型安全都归零。











