模糊搜索必须用#{}绑定参数,禁用${}拼接;推荐concat('%', #{name}, '%')或生成pattern;需校验空值并处理通配符转义。

模糊搜索接口最容易因拼接 % 而误用 ${} 或字符串拼接,直接导致 SQL 注入——只要用了 ${}、CONCAT() 且没校验输入,就基本等于把数据库裸奔上线。
MyBatis 里别用 ${} 做模糊匹配
这是最常见也最危险的写法。比如写成 WHERE name LIKE '%${name}%',攻击者传入 admin%' OR '1'='1,最终拼出 WHERE name LIKE '%admin%' OR '1'='1%',条件恒真。
-
${}是纯文本替换,不经过预编译,任何单引号、括号、注释符都会被原样塞进 SQL - 即使加了
trim或replace也拦不住嵌套注入,比如admin%' OR 1=1 -- - 唯一能用
${}的场景是动态表名、列名——但模糊查询不在此列
必须用 #{} + 数据库函数或 <bind></bind>
安全做法只有一条主线:所有用户输入必须走预编译参数绑定,% 符号要么由数据库生成(如 CONCAT),要么在 Java 层拼好再传入。
- 推荐写法:
WHERE name LIKE CONCAT('%', #{name}, '%')——#{name}被当作字符串参数传入,CONCAT在数据库侧执行,无注入风险 - 兼容性更强的写法:
<bind name="pattern" value="'%' + name + '%'"></bind> WHERE name LIKE #{pattern}—— 拼接发生在 MyBatis 解析阶段,不是 SQL 字符串拼接 - 避免在 Java 层手动加
%后直接用#{},比如queryWrapper.like("name", "%" + name + "%")是安全的,但前提是name本身没被污染(比如来自前端 raw 参数)
MyBatis-Plus 的 like 方法不是万能盾
queryWrapper.like("name", name) 确实默认前后加 %,但它只防“语法注入”,不防业务逻辑绕过。
- 它底层仍是
#{}绑定,所以不会被' OR 1=1破坏 SQL 结构 - 但如果
name是"a%b_c",而数据库字段含通配符_或%,可能意外匹配到不该匹配的记录(这不是注入,是语义误判) - 若需严格匹配,得配合
escape:比如queryWrapper.like("name", "a%b_c").escape('\'),并在 SQL 里声明ESCAPE '\'
额外检查点:边界输入和空值
很多模糊接口崩溃不是因为注入,而是因为没处理极端输入——这反而会暴露更多错误细节,给攻击者提供线索。
- 空字符串
""或空白符传入CONCAT('%', #{name}, '%')会变成%%,某些数据库(如旧版 MySQL)可能报错或返回全表 - 建议在 Controller 层做基础校验:
if (name == null || name.trim().isEmpty()) { throw new IllegalArgumentException("name 不能为空"); } - 更稳妥的做法是统一转为
Optional.ofNullable(name).filter(s -> !s.trim().isEmpty()).orElse(null),后端判null则跳过该条件
真正难防的不是 ' OR 1=1 这种教科书式 payload,而是当模糊字段参与多条件组合、关联子查询、或被用于 ORDER BY 时,#{} 失效而被迫用 ${} 的场景——那已经不是模糊搜索的问题,是整个查询设计需要重构。











