字符串拼接JPQL非参数值是否存SQL注入风险?如何解决?
先看你给出的代码:
final String query = "SELECT id FROM " + getSimpleName() + " WHERE " + relationName + ".id = :successor"; final Query queryConcerned = this.entityManager.createQuery(query); query.setParameter("successor", successorId);
Sonar给出的警告是:
Use a variable binding mechanism to construct this query instead of concatenation.
你提到自己拼接的不是参数值,而是表名和关联属性名,那这种写法到底有没有SQL注入风险?我们来拆解分析:
一、是否存在SQL注入风险?
答案是:取决于getSimpleName()和relationName的来源:
- 如果这两个值是硬编码常量、枚举值,或者来自你完全可控的内部元数据(比如从实体类的注解中读取的属性名),那当前场景下没有注入风险——攻击者根本没有机会篡改这些值。
- 但如果这两个值是来自外部输入(比如前端传参、用户配置、第三方接口返回),那风险极高!举个例子:如果
relationName被攻击者传入users WHERE 1=1 --,拼接后的SQL会变成:
这里的SELECT id FROM User WHERE users WHERE 1=1 -- .id = :successor--会注释掉后面的条件,最终查询会返回所有记录,完全绕过了原本的过滤逻辑,甚至可能被构造出更恶意的语句。
Sonar的警告其实是一种防御性提醒:它无法判断你拼接的内容是否绝对安全,所以默认禁止任何字符串拼接构建查询的写法——毕竟很多安全漏洞都是后续维护时引入的(比如某天有人把内部可控的relationName改成了前端传参)。
二、如何解决?
根据不同场景,推荐以下几种方案:
1. 使用JPA Criteria API(最推荐)
Criteria API是JPA提供的类型安全查询构建方式,完全不需要手动拼接字符串,JPA会自动处理表名、字段名的映射,从根源避免拼接风险:
CriteriaBuilder cb = entityManager.getCriteriaBuilder(); CriteriaQuery<Long> cq = cb.createQuery(Long.class); // 获取实体Root(这里可以直接传入实体Class,或者通过类名反射获取) Root<?> entityRoot = cq.from(Class.forName(getEntityFullName())); // 关联指定的属性(relationName是实体类中的属性名,JPA会自动映射到数据库字段) Join<?, ?> relationJoin = entityRoot.join(relationName); // 构建查询逻辑 cq.select(entityRoot.get("id")); cq.where(cb.equal(relationJoin.get("id"), cb.parameter(Long.class, "successor"))); // 执行查询 Query query = entityManager.createQuery(cq); query.setParameter("successor", successorId);
这种方式不仅安全,还能在编译期发现属性名拼写错误,可读性和可维护性都很强。
2. 白名单校验(适合必须用字符串拼接的场景)
如果一定要保留字符串拼接的写法,必须对拼接的内容做严格的白名单校验,确保只有合法的表名/关联名才能被拼接:
// 预定义所有允许的实体名和关联名(根据你的业务场景维护) Set<String> allowedEntityNames = Set.of("User", "Order", "Product"); Set<String> allowedRelationNames = Set.of("author", "customer", "seller"); String entityName = getSimpleName(); String relation = relationName; // 校验合法性,不合法直接抛出异常 if (!allowedEntityNames.contains(entityName)) { throw new IllegalArgumentException("Invalid entity name: " + entityName); } if (!allowedRelationNames.contains(relation)) { throw new IllegalArgumentException("Invalid relation name: " + relation); } // 校验通过后再拼接查询 final String query = "SELECT id FROM " + entityName + " WHERE " + relation + ".id = :successor"; final Query queryConcerned = this.entityManager.createQuery(query); query.setParameter("successor", successorId);
这种方式通过白名单彻底阻断了恶意值注入的可能,适合一些查询逻辑灵活但数据源可控的场景。
3. 使用JPA命名查询(适合固定逻辑的查询)
如果你的查询逻辑是固定的,可以在实体类上定义命名查询,完全避免拼接操作:
// 在对应的实体类上添加命名查询注解 @Entity @NamedQuery( name = "User.findIdByAuthorId", query = "SELECT u.id FROM User u WHERE u.author.id = :successor" ) public class User { // 实体属性... } // 代码中直接调用命名查询 Query query = entityManager.createNamedQuery("User.findIdByAuthorId"); query.setParameter("successor", successorId);
命名查询的可读性极强,而且完全由JPA管理,不存在拼接风险,适合查询逻辑固定的业务场景。
总结
哪怕你当前的拼接内容是安全的,Sonar的警告也是在提醒你这种写法存在潜在的维护风险。优先选择Criteria API这种类型安全的方式,其次用白名单校验兜底,尽量避免直接拼接字符串构建查询。
内容的提问来源于stack exchange,提问作者Renaud is Not Bill Gates

