只要没用字符串拼接sql,绝大多数场景下sql注入根本不会发生;但误用${}、order by直插字段名、string.format拼接等做法会引入深层漏洞,必须用参数化查询、白名单校验或criteria api防范。

只要没用字符串拼接 SQL,绝大多数场景下 SQL 注入根本不会发生——但很多人误以为“用了 JPA 或 MyBatis 就自动安全”,结果在 @Query 里写 ${}、在 ORDER BY 里直接插字段名、或在动态条件中用 String.format 拼 SQL,反而把漏洞藏得更深。
用 @Query 时别碰 ${},只用 :param 或 ?1
Spring Data JPA 的 @Query 支持两种占位符::param(命名参数)和 ?1(位置参数),它们都会被 Hibernate 转成 PreparedStatement 绑定;而 ${} 是纯字符串替换,完全绕过预编译。
- ✅ 安全写法:
@Query("SELECT u FROM User u WHERE u.status = :status AND u.name LIKE :pattern") - ❌ 危险写法:
@Query("SELECT u FROM User u WHERE u.status = :status ORDER BY ${sortBy}")——${sortBy}会被原样插入,攻击者传id; DROP TABLE user;就执行了 - ⚠️ 注意:即使加了
@Param("sortBy"),${}也不做任何转义,它不是参数,是模板引擎级别的文本替换
MyBatis-Plus 的 #{} 和 ${} 必须分清
#{} 对应 PreparedStatement 参数绑定,${} 对应字符串拼接——这是 MyBatis 最容易翻车的点,尤其在分页、排序、表名/列名动态化时。
- ✅ 查数据用
#{}:WHERE username = #{username} AND deleted = #{deleted} - ❌ 排序不能写
ORDER BY ${sortField},除非你白名单校验:if (!List.of("id", "created_time", "name").contains(sortField)) { throw new IllegalArgumentException(); } - ⚠️ 表名、列名、
IN子句里的值列表等无法参数化的部分,必须走白名单或正则严格校验,不能靠“用户应该不会乱输”来赌
动态 SQL 场景下,优先用 Criteria API 或 QueryDSL,而不是手拼
当查询条件高度可变(比如后台搜索页有 10 个可选字段),硬写 @Query 或 XML 易出错。Criteria API 虽啰嗦,但类型安全、全程参数绑定。
- ✅ JPA Criteria 示例:
criteria.select(root).where(builder.equal(root.get("type"), type), builder.like(root.get("title"), "%" + keyword + "%"))—— 所有值都走builder方法,不接触 SQL 字符串 - ❌ 避免:
"WHERE type = '" + type + "' AND title LIKE '%" + keyword + "%'"—— 这种写法哪怕套了@Service层也拦不住注入 - ⚠️ MyBatis-Plus 的
QueryWrapper是安全的(内部用反射+参数绑定),但别调用apply("xxx = xxx")直接塞原始 SQL 片段
过滤器里关键词拦截只是补丁,不是防线
像拦截 UNION SELECT、OR 1=1 这类规则,对简单攻击有效,但绕过方式极多(大小写混淆、注释切割、编码变形),且会误杀正常业务词(比如用户名含 “select”)。
- ✅ 可作为日志审计+告警辅助手段,比如记录含
;、/*、exec的请求并告警 - ❌ 别依赖它防注入:攻击者用
1' OR '1'='1就能绕过大多数关键词匹配 - ⚠️ 如果真要用过滤器,只清理 request body 和 query string 中的参数值,不要动 header 或 path variable;且清理后要重新 setAttribute,否则下游 Controller 拿不到干净值
真正难的是那些“不得不动态拼”的地方——比如多租户场景下切换 schema,或报表模块支持任意字段聚合。这些地方没有银弹,只能靠白名单 + 最小权限 DB 账号 + 严格审计日志三重兜底。别信“框架替你扛了”,它只扛得住你按规矩写的代码。










