位置:首页 > SQL > JPA动态SQL注入风险如何修复与安全防范

JPA动态SQL注入风险如何修复与安全防范

时间:2026-08-15  |  作者:穿越地图的猫  |  阅读:0

@Query + @Param 仅防参数值注入,无法防御动态表名、字段名、排序字段等SQL元数据注入,因其绕过PreparedStatement;必须通过白名单校验或Criteria API等机制保障安全。

如何修复JPA动态SQL中的注入风险

为什么 @Query + @Param 不能防住所有注入

@Query 配合 @Param 确实能防住参数值注入,但前提是整个 SQL 字符串是静态的。

一旦出现 ${}#{} 或运行时拼接表名/字段名,防线就会立刻失效。因为这些语法在 JPA 解析阶段就被展开为原始字符串,绕过了 PreparedStatement 的预编译机制。

常见错误场景包括:

  • @Query("SELECT * FROM user WHERE name LIKE '%${name}%'")
  • @Query(value = "SELECT * FROM #{#entityName}", nativeQuery = true)
  • @Query("SELECT * FROM user ORDER BY #{#sortField}")

这些写法会让恶意输入直接进入 SQL 结构层。数据库解析器会把它当指令执行,而不是值。

  • ${} 是 SpEL 字符串插值,等同于 Ja va 的 + 拼接
  • #{} 在 nativeQuery 中用于动态元数据(如表名),无法参数化
  • ORDER BY、GROUP BY、LIMIT 后的字段或数字,都不能用 :param 占位

动态表名/字段名必须走白名单校验

表名、列名、排序字段这些属于 SQL 元数据,JDBC 和 JPA 天然不支持参数化。

你不能写 SELECT :column FROM :table,运行时会报错。唯一安全的做法是:把允许的值固化为枚举或配置项,从用户输入映射到预设值,而不是直接代入。

例如排序字段只允许 "id""created_time""status"

这时就用 switchMap.of() 映射,遇到非法值直接抛 IllegalArgumentException

  • 禁止把 request.getParameter("sort") 直接传进 @Query 字符串
  • 白名单必须硬编码或加载自受控配置(如 @ConfigurationProperties),不能来自数据库或外部 API
  • SonarQube 报告的“SQL 注入”警告,在白名单场景下是误报,应配合 @SuppressWarnings("squid:CallToDeprecatedMethod") 或注释说明校验逻辑

复杂动态查询优先用 Criteria API 或 Specification

一旦查询条件组合变得很灵活,比如一个搜索页就有 5 个可选字段,同时又要守住类型安全,继续手写 @Query 往往很快就会难以维护。

相比之下,Criteria API 和 Spring Data JPA 的 Specification 不需要靠拼接字符串来组织查询,而是基于 Ja va 对象映射生成 SQL。

字段名、操作符都直接取自实体类定义,因此天生就具备注入防护能力。

示例中 root.get("username")"username" 是编译期确定的字符串字面量,不是运行时变量。

criteriaBuilder.like(...) 的值始终走 PreparedStatement 绑定。

  • 简单单条件查询,@Query + @Param 足够,别过度设计
  • 涉及 or/and 嵌套、权限过滤、多租户字段追加时,Specification 的组合能力(.and()/.or())比 if-else 拼 SQL 更可靠
  • 注意 Specification 不能替代白名单——它只管值和字段名,不管表名或排序方向(ASC/DESC

LIKE 查询和模糊匹配的写法陷阱

LIKE 的通配符(%)必须由代码拼,不能交给用户输入。

错误写法是:@Query("SELECT u FROM User u WHERE u.name LIKE :pattern") + @Param("pattern") "%"+input+"%"

这看似安全,但若 input 包含 _%,就会改变语义。更糟的是,有人写成 "%${input}%",直接破防。

更稳妥的方式是,把通配匹配这件事放在 Ja va 层处理,再把干净的参数值传下去。

JPA 本身就支持 criteriaBuilder.like(root.get("name"), "%" + name + "%") 这种写法;如果用原生查询,也可以写成 CONCAT('%', :name, '%')

但有一点必须卡死:% 的位置不能交给前端决定。

  • 用户输入的 name 应先做长度限制(如 ≤ 50 字符),防止慢查询
  • 若需支持前缀/后缀/全匹配,用独立参数区分,如 matchMode = "prefix",再由服务端决定加几个 %
  • 避免在 SQL 里用 ESCAPE 处理特殊字符——增加复杂度且易出错,不如在 Ja va 层 clean 输入

结论

真正难的不是写对一行 @Query,而是识别哪些地方“看起来像参数,其实不是参数”。

表名、字段名、排序方向、分组维度、函数名 这些都要在代码里划清边界。

应通过白名单把它们卡死,而不是依赖框架的“自动防护”。

免责声明:文中图文均来自网络,如有侵权请联系删除,心愿游戏发布此文仅为传递信息,不代表心愿游戏认同其观点或证实其描述。

相关文章

更多

精选合集

更多

大家都在玩

热门话题

大家都在看

更多