like模糊查询防注入的关键是参数化:必须用concat('%', ?, '%')配合ps.setstring(1, userinput),而非字符串拼接或${};因?经预编译作纯数据处理,而拼接会使%、_、单引号等被当作sql语法执行。

LIKE 模糊查询本身不危险,危险的是把用户输入直接拼进 SQL 字符串里——哪怕只加了 %,只要没走参数化,就等于把钥匙交给攻击者。
为什么 WHERE name LIKE '%'+userInput+'%' 必然被注入
因为 % 和 _ 是 SQL 语法字符,不是普通数据;而单引号、反斜杠、OR、-- 等更会直接破坏语句结构。一旦用字符串拼接,JDBC 或数据库驱动根本分不清哪部分是代码、哪部分是数据。
- 用户输入
' OR 1=1 --→ 实际执行:WHERE name LIKE '%' OR 1=1 -- %',条件恒真 - 用户输入
abc%def→ 查询变成“以 abc 开头”,而非“字面含 abc%def” - 用户输入
a_c→ 匹配abc、axc,语义被通配符劫持
为什么 CONCAT('%', ?, '%') 能防注入,但 CONCAT('%', '${userInput}', '%') 不行
CONCAT 函数本身不防注入,它只是个工具;真正起作用的是 ? 占位符背后的预编译机制——数据库把整个 CONCAT 当作函数调用,所有 ? 参数都作为纯数据传入,不参与 SQL 解析。
- ✅ 正确:SQL 写成
WHERE name LIKE CONCAT('%', ?, '%'),再用ps.setString(1, userInput) - ❌ 错误:SQL 写成
"WHERE name LIKE CONCAT('%', '" + userInput + "', '%')",拼接发生在 Java 层,userInput已经裸奔 - ❌ MyBatis 中写成
LIKE CONCAT('%', ${keyword}, '%'),${}是字符串替换,等同于拼接
MyBatis 里 #{} 和 ${} 在 LIKE 场景下的关键区别
#{} 触发预编译,${} 是文本替换——这是分水岭,不是语法糖差异。
-
AND name LIKE CONCAT('%', #{keyword}, '%')→ 安全,#{keyword}被当作参数值传给CONCAT -
AND name LIKE '%${keyword}%'→ 高危,${keyword}直接插进 SQL 字符串,引号、%、_全部失控 -
AND name LIKE #{pattern}→ 安全,但要求 pattern 值由后端构造(如"%" + keyword.replace("!", "!!").replace("%", "!%") + "%"),且 SQL 中必须带ESCAPE '!'
MySQL 低版本或需要精确匹配 %_ 时的替代方案
如果业务要求用户能搜字面量 abc% 或 a_c,光靠 CONCAT 不够,必须启用 ESCAPE 并在代码层转义。
- 选一个转义符,比如
!,先将用户输入中的!替换为!! - 再把
%→!%,_→!_ - SQL 写成:
WHERE name LIKE !%{escapedKeyword}!% ESCAPE '!' - 注意:不能写成
WHERE name LIKE '%#{keyword}%' ESCAPE '!',因为%写死在 SQL 模板里,#{keyword}就不再是完整模式串
最常被忽略的一点:CONCAT 防注入的前提是整条 SQL 走 PreparedStatement,而不是“用了 CONCAT 就万事大吉”。只要混用字符串拼接或 ${},前面所有努力都会归零。











