作者:tkswifty
来源:SecIN社区
实际业务场景中经常需要执行limit分页语句,返回部分数据。物理分页只返回部分数据占用内存小,能够获取数据库最新的状态,实时性比较强,一般适用于数据量比较大,数据更新比较频繁的场景。
MybatisPlus插件提供了相应的支持。使用PaginationInnerInterceptor插件可以十分方便的完成相应的功能。
一、关于PaginationInnerInterceptor
PaginationInnerInterceptor作为plus的分页插件,提供了通用的参数进行统一配置。可以很方便的完成分页的业务逻辑。
1.1 相关依赖
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-extension</artifactId>
<version>3.4.3</version>
</dependency>
1.2 Springboot集成
主要是通过拦截器的方式配置分页插件:
@Configuration
public class MyBatisPlusConfig {
@Bean
public PaginationInterceptor paginationInterceptor() {
return new PaginationInterceptor();
}
}
新版本改用了MybatisPlusInterceptor
,配置如下:
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
定义好mapper后就可以直接调用对应的方法进行操作了。
最常用的是直接继承BaseMapper接口,无需编写mapper.xml文件,即可获得CRUD功能。其中mybatis-plus提供的分页方法是selectPage和selectMapsPage,其中一个入参为com.baomidou.mybatisplus.extension.plugins.pagination.page
,以下是具体的function定义:
/**
* 根据 entity 条件,查询全部记录(并翻页)
*
* @param page 分页查询条件(可以为 RowBounds.DEFAULT)
* @param queryWrapper 实体对象封装操作类(可以为 null)
*/
<E extends IPage<T>> E selectPage(E page, @Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
/**
* 根据 Wrapper 条件,查询全部记录(并翻页)
*
* @param page 分页查询条件
* @param queryWrapper 实体对象封装操作类
*/
<E extends IPage<Map<String, Object>>> E selectMapsPage(E page, @Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
可以看到,直接调用对应的function即可完成相关的业务需求:
@GetMapping("/userList")
public String userList(@RequestParam(value = "pn", defaultValue = "1")Integer pn, Model model) {
// 分页查询数据
Page<User> userPage = new Page<>(pn, 2); //pn:第几页 size:每页记录条数
Page<User> page = userService.page(userPage, null);
model.addAttribute("users", page);
long current = page.getCurrent(); // 当前页码
long pages = page.getPages(); // 总页数
long total = page.getTotal(); // 总记录数
......
}
另外,对于使用xml和注解自定义的mapper方法,传递参数 Page 即自动分页,且参数必须放在第一位。
二、分页参数page
具体的分页是通过配置Page对象相关的参数实现的。同时,不同阶段的版本com.baomidou.mybatisplus.extension.plugins.pagination.page
实现是不一样的。主要区别在于Orderby排序字段上。
2.1 ascs/descs
mybatis-plus-extension-3.1.1及以下版本
通过控制ascs或者descs来进行分页排序(3.1.1版本):
public class Page<T> implements IPage<T> {
private static final long serialVersionUID = 8545996863226528798L;
private List<T> records;
private long total;
private long size;
private long current;
private String[] ascs;
private String[] descs;
private boolean optimizeCountSql;
private boolean isSearchCount;
......
}
2.2 List<OrderItem> orders
mybatis-plus-extension-3.1.2及以上版本
通过List集合orders来进行分页排序(3.4.3版本),也可以通过setAsc、setDescs方法来配置,但是相关方法已弃用:
public class Page<T> implements IPage<T> {
private static final long serialVersionUID = 8545996863226528798L;
protected List<T> records = Collections.emptyList();
protected long total = 0L;
protected long size = 10L;
protected long current = 1L;
protected List<OrderItem> orders = new ArrayList<>();
protected boolean optimizeCountSql = true;
protected boolean searchCount = true;
protected String countId;
protected Long maxLimit;
public void setOrders(List<OrderItem> orders) {
this.orders = orders;
}
......
}
orders集合封装了OrderItem,在OrderItem中包含了排序分页所需要的字段内容,默认为asc正序排序:
public class OrderItem implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 需要进行排序的字段
*/
private String column;
/**
* 是否正序排列,默认 true
*/
private boolean asc = true;
public static OrderItem asc(String column) {
return build(column, true);
}
public static OrderItem desc(String column) {
return build(column, false);
}
......
}
三、PaginationInnerInterceptor使用不当导致的SQL注入
跟所有的框架插件一样,只要涉及到数据库交互,使用不当就会导致SQL注入的安全风险。
3.1 Orderby场景下的SQL注入
前面提到了分页中会存在Orderby的使用,因为Orderby动态查询没办法进行预编译,所以不经过安全检查的话会存在注入风险。PaginationInnerInterceptor主要是通过设置com.baomidou.mybatisplus.extension.plugins.pagination.page
对象里的属性来实现orderby的,主要是以下函数的调用,因为直接使用sql拼接,所以需要对进行排序的列名进行安全检查:
page.setAsc();
page.setDesc();
page.setAscs();
page.setDescs();
page.setOrders();
page.addOrder();
3.2 Spring自动绑定导致的潜在SQL注入风险
3.2.1 漏洞原理
在 Spring框架中,提交请求的数据是通过方法形参来接收的。从客户端请求的 key/value 数据,经过参数绑定,将 key/value 数据绑定到 Controller 的形参上,然后在 Controller 就可以直接使用该形参。
在实际开发场景中,开发人员可能通过直接传递com.baomidou.mybatisplus.extension.plugins.pagination.page
来实现业务,例如下面的例子:
@RequestMapping(value = "/getPage")
public String getPage(Page page) {
Page<User> userList =userMapper.selectPage(page, new QueryWrapper<User>().lambda().eq(User::getId, 1));
......
}
查看Page的属性,直接在request请求时候传入size和current两个参数就可以完成对应的分页需求了,前端的确也是这么设计的:
public class Page<T> implements IPage<T> {
private static final long serialVersionUID = 8545996863226528798L;
protected List<T> records; //分页对象记录列表
protected long total; //总记录跳数
protected long size; //每页显示条数
protected long current; //当前页码
protected List<OrderItem> orders; //排序信息
protected boolean optimizeCountSql; //自动优化 COUNT SQL【 默认:true 】
protected boolean isSearchCount; //进行 count 查询 【 默认: true 】
protected boolean hitCount; //设置是否命中count缓存
protected String countId; //MappedStatement 的 id
protected Long maxLimit; //最大每页分页数限制,优先级高于分页插件内的 maxLimit
......
}
根据spring自动绑定的特性,若此时加入orders参数的传递,同样的后端会进行对应的实体封装,最终带入到sql查询中,同时因为order by场景下MybatisPlus并没有相关的安全措施,会导致SQL注入风险。
3.2.2 利用方式
以上面的代码为例,看看具体的利用方式。同时前面也提到了不同版本com.baomidou.mybatisplus.extension.plugins.pagination.page
对象里排序相关的属性会不一样,这里结合常见的reqeust提交方式分情况讨论(主要是普通的post和json请求):
根据自动绑定的特性,可以将page对象中相关的属性为参数(前面第二节提到的ascs、descs以及List<OrderItem> orders),追加在request中,尝试利用,这里以H2 database为例,通过注入恶意sql请求dnslog来进行验证:
3.2.2.1 application/x-www-form-urlencoded
ascs/descs
以descs为例,其参数类型是String[],直接传递即可:
通过print相关的sql日志可以看到追加的内容id,1/0成功引入到sql查询中,同时返回除0错误,说明SQL注入利用成功:
这里有个需要注意的点是,因为descs的数据类型是是String[],逗号会自动进行分隔,在实际利用的时候会比较麻烦。
List<OrderItem> orders
因为自动绑定的内容是List,在传参时可以通过entity[index].attribute的方式进行传递:
3.2.2.2 application/json
除了上述的情况,经常会有通过@RequestBody注解来传递JSON内容的情况:
@RequestMapping(value = "/getPageByJson")
public String getPage1(@RequestBody Page page) {
Page<User> userList =userMapper.selectPage(page, new QueryWrapper<User>().lambda().eq(User::getId, 1));
......
}
ascs/descs
以descs为例,其参数类型是String[],可以通过[content,content]的方式进行传递,跟application/x-www-form-urlencoded不一样,因为相关字段内容用“”圈定了,所以上下文的逗号不会相互影响:
List<OrderItem> orders
通过前面查看源码,可以知道orders参数的类型是List集合。
@RequestBody可以通过如下方式来自动绑定list集合:
同样的print相关日志,可以看到恶意的sql语句成功写入:
3.2.3 其他
有时候用api级别的方法不能满足分页的需求,比如多张表关联查询的时候,这个时候就需要自定义分页了。对于使用xml和注解自定义的mapper方法,传递参数 Page 即自动分页(且参数必须放在第一位),同样存在类似的问题。例如下面的例子:
public interface UserMapper extends BaseMapper<User> {
@Select({"< script>",
"SELECT id, name, age, email FROM user where id=#{id}</script>"})
IPage<User> queryPageById(Page<?> page, @Param("id")long id);
}
同理,在controller直接调用mapper对应的function即可:
@RequestMapping(value = "/getPage")
public String getPage(Page page,long id) {
IPage<User> userlist = userMapper.queryPageById(page,id);
....
}
因为直接传递了Page实体,所以同样的可以直接在相关的参数写入恶意sql语句,通过自动绑定即可利用,数据库为h2 database,这里通过请求dnslog进行验证:
可以看到dnslog成功记录到请求,sql注入利用成功:
综上所述,由于MybatisPlus分页插件的使用方法不当,导致了SQL注入风险。
四、其他
对于上述情况,在实际开发时需要额外的注意。可以通过如下方法来规避自动绑定导致的SQL注入问题。
可以修改Controller层,仅仅接受用户可以控制的参数,若需要使用orderby排序相关的参数,加入相关的安全检查即可:
@GetMapping("/userList")
public String userList(@RequestParam(value = "pn", defaultValue = "1")Integer pn, Model model) {
// 分页查询数据
Page<User> userPage = new Page<>(pn, 2); //pn:第几页 size:每页记录条数
Page<User> page = userService.page(userPage, null);
......
}
也可以通过设置@InitBinder可以绑定的白名单,但是例如List<OrderItem> orders不是简单的类型,需要定义相应的Editor进行处理。
来源:freebuf.com 2021-06-25 10:23:49 by: SecIN技术社区
请登录后发表评论
注册