MyBatis中禁用${},除非经白名单校验;所有普通参数用#{},动态列名/排序字段需映射安全字段;JDBC和JPA中 likewise 避免字符串拼接,LIKE/IN等须用函数或foreach处理。

MyBatis里别用${},除非你手动白名单校验
只要用了${},哪怕只在ORDER BY或IN子句里出现一次,就等于把SQL解析权交给了用户输入。它不走PreparedStatement,不防注入,也不做类型检查。
常见错误场景:
-
WHERE name = '${name}'→ 输入admin' OR '1'='1直接绕过条件 -
ORDER BY ${sortField}→ 输入id; DROP TABLE user;可能触发多语句执行(取决于数据库配置)
正确做法:
- 所有普通参数一律改用
#{},让MyBatis自动绑定为预编译参数 - 动态列名/表名/排序字段等必须白名单控制:定义
Map<String, String> safeFields = Map.of("name", "user_name", "time", "create_time"),再用safeFields.get(sortKey)取值,取之前先判断key是否存在 - XML里全局搜
$——IDE用Ctrl+Shift+F搜索\$\{(注意转义),逐个确认是否真有必要
JdbcTemplate必须用问号占位符,别拼字符串
手写JDBC时,Statement + 字符串拼接是高危操作;JdbcTemplate本身不防注入,关键看你怎么调用它。
危险写法:
-
"SELECT * FROM user WHERE name = '" + name + "'"→ 直接执行,无任何防护 -
jdbcTemplate.query(sql, new Object[]{name}, ...)但sql本身含拼接变量
安全写法:
- SQL语句固定,仅参数用
?占位:"SELECT * FROM user WHERE status = ? AND deleted = ?" - 传参用
Object[]或Map,确保每个?对应一个干净值 - 模糊查询别写
"%"+keyword+"%"再塞进SQL——应保持占位符结构:"WHERE name LIKE ?",然后传入new Object[]{"%" + keyword + "%"}
@Query里禁用${},命名参数才是安全的
Spring Data JPA的@Query支持:param和?1,它们会被Hibernate转成预编译参数;而${}是模板引擎级替换,完全跳过ORM层校验。
典型翻车点:
-
@Query("SELECT u FROM User u ORDER BY ${sortBy}")→sortBy=id; DROP TABLE user可直接执行 - 即使加了
@Param("sortBy"),${}也不认这个注解,它只认字符串字面量
替代方案:
- 排序字段走白名单校验后,用
Sort.by(Sort.Direction.ASC, safeField)传给Pageable - 复杂动态查询优先用
CriteriaBuilder,比如builder.like(root.get("title"), "%" + keyword + "%"),全程不拼SQL字符串 - 真要动态字段,就在Service层用
if-else分支生成不同@Query方法,而不是靠${}一把梭
Danger zone:LIKE、IN、表名这些“没法参数化”的地方
很多人以为#{}不能用在LIKE或IN里就去换${},这是拿一个漏洞换另一个漏洞。
标准解法:
-
LIKE:MySQL用CONCAT('%', #{keyword}, '%'),PostgreSQL用'%' || #{keyword} || '%',保持#{}语义 -
IN:必须用<foreach>标签(MyBatis)或IN :ids+@Param("ids") List<Long>(JPA),让框架为每个元素生成独立占位符 - 表名/列名:只能白名单或正则校验,例如
Pattern.compile("^[a-zA-Z_][a-zA-Z0-9_]*$").matcher(tableName).matches(),且校验必须在SQL拼接前完成
真正容易被忽略的是:DTO校验必须生效。光加@Valid没用,字段得配@Size、@Pattern,否则校验器根本不会跑;全局异常处理器也得捕获DataAccessException,防止SQL报错泄露表结构。


















