Java代审之SQL注入—SpringDataJpa

admin 2021年5月7日00:46:02评论530 views字数 7877阅读26分15秒阅读模式

Spring Data Jpa简介

  JPA(Java Persistence API)是一种Java持久化解决方案,负责把数据保存到数据库,实际上它就是一种标准、规范,而不是具体的实现。主要是为了简化持久层开发以及整合ORM技术,结束Hibernate、TopLink、JDO等ORM框架各自为营的局面。JPA是在吸收现有ORM框架的基础上发展而来,易于使用,伸缩性强。

 Spring Data JPA 是spring在基于ORM框架、JPA规范的基础上封装的一套JPA框架(Repository层的实现),可以令开发者使用极简的代码实现对数据的访问和操作。其简化了对DAO层代码的编写,摆脱了对数据库的CRUD等基本操作,开发人员在DAO层中只需要写接口,就自动具有了增删改查、分页查询等方法。

Spring Data Jpa框架整合

  以Springboot整合为例,引入Spring Data Jpa依赖:

xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

  在application.properties配置数据源以及JPA:

```reStructuredText
spring.datasource.url=jdbc:mysql://localhost:3306/test
spring.datasource.username=root
spring.datasource.password=password
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

update更新表结构,如果不存在则创建

spring.jpa.properties.hibernate.hbm2ddl.auto=update

控制台输出JPA自动生成的sql语句

spring.jpa.show-sql=true

设置在当前项目是否运行quartz定时任务

quartz.enabled=true
```

  然后是编写实体类并通过继承Repository的方式创建JPA接口类:

  • JpaRepository(提供的简单数据操作接口)

  • JpaSpecificationExecutor(提供的复杂查询接口)

  • CrudRepository(提供了最基本的对实体类的增、删、改、查操作)

  • PagingAndSortingRepository (提供进行分页和排序记录的方法)

  • ......

  此时即可完成Spring Data Jpa整合,然后完成相关CURD操作了:

java
public interface CustomerRepository extends JpaRepository<实体Dao, 数据类型> {
}

  也就是说,可以通过检查相关的依赖以及检索Repository关键字来定位jpa框架

常见的SQL交互方式

Repository自带方法

  实现了JPA接口类后,可以通过Repository自带的一些方法完成简单的CURD操作。例如从CrudRepository继承的count, delete,deleteAll, exists, findOne, save等方法。

  例如如下例子,根据long类型的Id,返回对应的Customer:

java
Customer result = repository.findOne(1L);

  通过spring.jpa.show-sql=true输出jpa生成的sql语句,可以看到熟悉的占位符?,也就是说整个交互过程是SQL预编译的,一定程度上防止了SQL注入:

sql
Hibernate: select customer0_.id as id1_0_0_, customer0_.first_name as first_na2_0_0_, customer0_.last_name as last_nam3_0_0_ from customer customer0_ where customer0_.id=?

自定义Repository方法查询

  与此同时,还可以通过创建查询方法来交互,当然了需要满足一定的规范:

  • 查询方法需要以 find | read | get开头
  • 涉及查询条件时需要用条件关键字连接
  • 属性首字母大写
  • 支持级联属性

  例如下面的例子:

| 关键词 | 案例 | JPA生成SQL |
| ------ | --------------- | ------------------------------ |
| And | findByAgeAndSex | ...where x.age=?1 and x.sex=?2 |

  也就是说,只要在Repository中创建如下方法即可实现根据age和sex查询用户信息的需求,同时相关的交互SQL也是使用?进行预编译的,杜绝了SQL注入的问题:

java
/**
* 根据age和sex查询用户信息
* @param lastName
* @return
*/
List<Customer> findByAgeAndSex(String age,String sex);

使用注解

@NamedQuery和@NamedNativeQuery

  • 通过XML配置<named-query>或在实体Dao处配置@NamedQuery(基于JPQL)
  • 通过XML配置<named-native-query>或在实体Dao处配置@NamedNativeQuery(基于原生SQL)

  注意sql表达式里的表名和相关的参数要和当前的Entity一致,否则会抛出异常:

  例如下面的例子,在@Entity下增加相关的注解,

java
@Entity
@NamedQuery(name = "Category.selectByName",query = "SELECT c FROM Category c WHERE c.categoryName = ?1 ")
@NamedNativeQuery(name = "Category.selectByNameLike",query = "SELECT * FROM cfq_jpa_category WHERE category_name LIKE ?1 ",resultClass = Category.class)
public class Category {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
......
}

  然后在Repository接口中声明对应的方法selectByNameselectByNameLike,最后在相关业务Controller直接调用Repository的相关方法即可完成相关的SQL操作:

java
List<Category> categoryList = categoryRepository.selectByNameLike("%测试%");

  同样的在定义sql表达式的时候使用?占位符进行预编译,防止了SQL注入风险。但因为这种方式在实体Dao处引入了过多的配置,一般情况下使用的比较少。

@Query

  @Query注解主要在实现的Repository接口添加,同样的也支持JPQL和原生SQL的方式(使用原生SQL需要在@Query注解中设置nativeQuery=true,然后value变更为原生SQL即可)。

  例如下面的例子;

```java
@Query("select c from Customer c where c.firstName like %:name")
List findByName2(@Param("name") String name);

@Query(nativeQuery = true,value = "select * from Customer c where c.first_name like concat('%' ,?1,'%') ")
List findByName3(@Param("name") String name);

@Query("select c from Customer c where c.firstName=?1")
Customer findByFirstName2(String bauer);
```

  同样的在相关业务Controller直接调用Repository的相关方法即可完成相关的SQL操作。这里也使用了?:占位符进行预编译操作,不同的是?代表在方法参数里的位置(从1开始),:变量名这种方式是跟方法中@Param修饰的参数匹配,均可达到防止SQL注入的效果。

@Modifying

  可以通过@Modifying结合@Query进行update操作。例如下面的例子,通过用户名userName来修改昵称nickName,同时返回更改数据的行数:

java
@Modifying//更新查询
@Transactional//开启事务
@Query("update Customer c set c.nickName = ?1 where c.userName = ?2")
int setFixedFirstnameFor(String nickName, String userName);

  这里也使用了?占位符进行预编译操作,防止SQL注入风险。

  以上方式为Spring Data Jpa实现简单SQL交互的过程,整个实现过程不用像mybatis一样编写mapper文件,不用各种配置,也不用定义相关的增删改查方法,通过编写对应的Dao,继承实现对应的Repository,即可执行对应的SQL逻辑。同时内部已经进行了相关预编译的操作,十分的方便。

使用 Specification进行动态SQL查询

  针对一些复杂的查询,上述方法可能无法满足,此时一般在repository中继承JpaSpecificationExcutor接口,然后通过CriteriaQuery来实现复杂的逻辑(本质上还是封装了EntityManager的使用)。因为实际调用criteriaBuilder提供的in、like等查询方式,所以同样的在一定程度上也解决了SQL注入的问题,例如下面的例子:

java
public static Specification containsLike(String attribute, String value) {
return (root, query, cb) -> cb.like(root.get(attribute), "%" + value + "%");
}

  具体调用:

java
Specification<Customer> spec = SpecificationFactory.containsLike("lastName","bau");
Pageable pageable = new PageRequest(0,5, Sort.Direction.DESC,"id");
Page<Customer> page = csr.findAll(spec,pageable);

  相关的SQL同样使用了占位符进行预编译处理:

Java代审之SQL注入—SpringDataJpa

  以上方式为Spring Data Jpa实现简单SQL交互的过程,整个实现过程不用像mybatis一样编写mapper文件,不用各种配置,也不用定义相关的增删改查方法,通过编写对应的Dao,继承实现对应的Repository,即可执行对应的SQL逻辑。同时内部已经进行了相关预编译的操作,十分的方便。

常见SQL注入场景

  Spring Data Jpa提供了相关的封装来实现SQL交互的过程,同时内部已经进行了相关预编译的操作(例如上面案例中的like模糊查询、update更新、查询等操作),大大减少了SQL注入缺陷的可能。要寻找SQL注入,本质还是查看相关参数是否可控以及是否进行了SQL拼接。可以结合一些注入高频场景来挖掘。

Order by以非实体属性进行排序

  采用预编译执行SQL语句传入的参数不能作为SQL语句的一部分,那么Order By后的字段名、或者是descasc也不能预编译处理,那么也就是说Order By场景的业务实现还是只能使用拼接。下面看看Spring Data Jpa的处理方式。

  首先PagingAndSortingRepository提供了排序以及分页查询的方法:

java
Iterable<T> findAll(Sort var1);
Page<T> findAll(Pageable var1);

  其中Sort主要为了实现排序功能,内有枚举类指示排序方式(DESC、ASC),Pageable是一个接口,提供了分页一组方法的声明(第几页,每页多少条记录,排序信息等)。

  但是很多时候上述方法不能满足实际业务需要,例如需要对查询结果进行排序,那么就需要结合@Query注解,编写JPQL或原生SQL表达式,使用Sort或者Pagealbe来对结果进行排序。例如下面的例子:

  在Repository里添加相关方法:

```java
@Query(value = "select * from Customer n#pageablen",
countQuery = "select count(*) from Customer",
nativeQuery = true)
Page findAllRandom(Pageable pageable);

@Query("select c from Customer c where c.firstName=:name or c.lastName=:name")
List findByName4(@Param("name") String name2,Sort sort);
```

  使用JPQL进行交互,直接调用上述方法即可,这里尝试进行简单的SQL注入判断,让其尝试以id,1进行排序:

java
Pageable pageable = new PageRequest(0,3, Sort.Direction.DESC,"id,1");
Page<Customer> page = repository.findByName("Bauer", pageable);

  若存在SQL注入,当列数超出的时候,例如id,100时会抛出异常,尝试通过框架执行上述过程,抛出如下异常:

Java代审之SQL注入—SpringDataJpa

  其认为排序的字段id,1与实体定义的字段不匹配,所以抛出异常了。这里说明框架在进行Order by排序时实现了间接引用,只允许实体属性匹配的内容进行排序。同理,nativeQuery = true修饰的原生SQL也是一样的。这样在一定程度上也防御了SQL注入风险。

  但是实际情况有时候需要用实体之外的字段对查询结果排序,那么上述方法就无法满足业务需求了。根据前面的报错信息,需要使用JpaSort.unsafe()进行处理,以nativeQuery = true修饰的原生SQL方法为例:

java
Pageable pageable = new PageRequest(0,3, JpaSort.unsafe(Sort.Direction.DESC,"id,100"));
Page<Customer> page = repository.findAllRandom(pageable);

  尝试对id,100进行排序,返回了列数超出的错误,此时说明可以进行SQL注入了:

Java代审之SQL注入—SpringDataJpa

  使用sqlmap进一步验证:

Java代审之SQL注入—SpringDataJpa

  JPQL场景同理,只是没办法跟原生SQL修饰的一样可以通过跨表查询等方式进行深入利用。
  综上可得到其中一个场景:

  • Order by以非实体属性进行排序(直接检索是否使用了JpaSort.unsafe()方法且参数内容是否可控来进行判断

EntityManager动态拼接sql语句

  spring-data-jpa对于简单的数据操作确实使用起来比较方便,但是对于一些比较复杂的动态的多表条件查询就不是那么友好了,对于需要些sql语句并且需要动态的添加条件的时候就得使用jpa的EntityManager来完成了。同样的,如果存在SQL拼接的话,还是会存在注入风险的。
emsp; 例如下面的例子:

  首先在service曾引入EntityManager:

java
@PersistenceContext
EntityManager entityManager;

  然后使用entityManager.createNativeQuery()来执行原生的SQL语句:

java
Pageable pageable = PageUtils.getPageable(pageIndex,pageSize);
StringBuilder sql = new StringBuilder();
sql.append(" select ep.name,MAX(r.sign) from mnt_emp_rel_meeting as e ");
sql.append(" left join mnt_sign_record as r on(r.employee_id=e.employee_id) ");
sql.append(" left join mnt_employee as ep on(ep.id = e.employee_id) ");
sql.append(" where e.meeting_id ='"+meetingId"'");
sql.append(" order by r.sign desc,r.create_date asc ");
Query query = entityManager.createNativeQuery(sql.toString());
query.setFirstResult(pageable.getOffset());
query.setMaxResults(pageable.getPageSize());
List<Object[]> list = query.getResultList();

  可以看到这里meetingId是通过SQL拼接的方式进行查询,若meetingId用户可控的话,会存在SQL注入风险。

  正确的做法应该是通过占位符的方式进行预编译处理,类似Order by等无法预编译处理的场景也需间接对象引用:
java
sql.append(" where e.meeting_id =?");
......
query.setParameter(1,meetingId);

  当然项目中用的是其他的ORM实现,也提供了对应的实现,例如如下hql的:

java
Query q = entityManager.createNativeQuery(hql.toString());
query.unwrap(org.hibernate.SQLQuery.class)
.setResultTransformer(Transformers.ALIAS_TO_ENTITY_MAP);

  综上可得到另外一个场景:

  • EntityManager动态拼接sql语句(直接检索EntityManager的相关使用

参考资料

https://github.com/icnws/spring-data-jpa-demo

相关推荐: 如何在Node.js中逃逸vm沙箱

vm基本用法 vm模块可在V8虚拟机上下文中编译和运行nodejs代码。按照官方文档的说法,vm不是一个安全的机制,并不适合用来运行不受信任的代码。 vm的一个常见用法是做上下文隔离: ```javascript const vm = require('vm'…

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2021年5月7日00:46:02
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Java代审之SQL注入—SpringDataJpahttps://cn-sec.com/archives/246705.html