@query中拼字符串高危,因原生sql仅占位符预编译,其余部分字面量执行;动态列名、表名、排序字段无法参数化,必须白名单校验或硬编码。

只要不拼字符串,就基本不会被注入;但动态列名、表名、排序方向这些根本不能参数化,必须靠白名单或硬编码兜底。
为什么 @Query 里拼字符串是高危操作
原生 SQL 的 @Query(nativeQuery = true) 只对 :param 或 ?1 这类占位符做预编译,其余部分全当字面量交给 JDBC 执行。一旦你写成 "SELECT * FROM user WHERE name = '" + name + "'",用户输 admin' OR '1'='1 就直接查出全表。
- SpEL 表达式
#{#name}同样是运行时字符串替换,等价于拼接 -
IN子句不能用WHERE id IN :ids—— JPA 不支持原生 SQL 中数组参数展开,必须手动展开为IN (?1, ?2, ?3) - 动态表名、列名、
ORDER BY ... :sortDir全部非法:JPA 规范禁止参数化标识符,运行时报错或查不到数据
用 Specification 替代手拼 JPQL
有人在 Service 层用 StringBuilder 拼 JPQL 再塞进 @Query,这是典型破防:因为 @Query 是编译期固定的,所谓“动态”只能靠运行时字符串拼接,而拼接即注入温床。
- 正确做法是实现
Specification,例如cb.like(root.get("name"), "%" + name + "%")—— 这里的name是纯参数值,%写死在代码里,不来自用户输入 - 嵌套条件、多表关联、复杂
OR/AND分组,都可用Specifications.where().and().or()安全组合 - 每个条件封装为独立谓词,可复用、可测试、类型安全,最终仍生成预编译语句
动态列名去重必须走原生 SQL,但要严控输入源
JPQL 不允许参数化列名,所以 @Query("SELECT DISTINCT t.:columnName FROM ...") 必然失败。唯一可行路径是 nativeQuery = true,但 Spring 对 :columnName 和 :tableName 不做任何校验或转义。
-
tableName必须是数据库中真实存在的物理表名(非实体类名) -
columnName必须为该表中合法的列名,建议用白名单校验:Set.of("status", "type", "created_date").contains(input) - 更稳妥的做法是 Service 层用
JdbcTemplate手动拼 SQL,自行处理引号包裹(如 PostgreSQL 用双引号、MySQL 用反引号),并拒绝任何非字母数字+下划线的输入
最易被忽略的是:动态排序字段、GROUP BY 列、HAVING 条件中的字段名,全部无法参数化。它们和表名一样,必须走白名单或配置驱动,绝不能直通用户请求参数。










