解决mybatis-plus动态排序,导致的SQL注入问题

解决mybatis-plus动态排序,导致的SQL注入问题

关于什么是SQL注入,需要先自行通过搜索引擎了解

mybatis-plush提供的动态排序API

setOrderBy() 可以设置一个排序的语句,用于动态的排序

PageHelper.startPage(1, 10).setOrderBy("`id` DESC");

List<FooEntity> foos = this.fooMapper.foos();

foos.forEach(System.out::println);
  • SQL日志
    SELECT * FROM `foo` order by `id` DESC LIMIT ?

  • 输出的结果

FooEntity [id=5, name=E]
FooEntity [id=4, name=D]
FooEntity [id=3, name=C]
FooEntity [id=2, name=B]
FooEntity [id=1, name=A]

SQL注入问题

动态排序的 order by 语句,并不是预编译的。而且多是由前端提交参数。可能存在SQL注入的问题

// 恶意构造一条删除的SQL语句
PageHelper.startPage(1, 10).setOrderBy("`id` DESC; DELETE FROM `foo` WHERE `id` = 1; SELECT 1 FROM `foo`");
List<FooEntity> foos = this.fooMapper.foos();
foos.forEach(System.out::println);

SQL日志

可以看到,执行检索后,成功的删除掉了一条记录

FooMapper.foos         : ==>  Preparing: SELECT * FROM `foo` order by `id` DESC; DELETE FROM `foo` WHERE `id` = 1; SELECT 1 FROM `foo` LIMIT ? 
FooMapper.foos         : ==> Parameters: 10(Integer)
FooMapper.foos         : <==      Total: 5
FooMapper.foos         : <==    Updates: 1

解决办法

通过搜索引擎可以发现很多解决方案。

使用枚举固定排序的字段

就是把需要的排序策略,写成枚举类。根据前端提供的参数,来选择枚举。进行排序。这种方法可以避免由客户端直接提交数据生成SQL。但是缺点就是不够灵活。当字段更改的试试,需要修改代码。

enum Order {
	NAME_ASC("`name` ASC"),
	ID_DESC("`id` DESC")
 	...
}

使用正则过滤

其实排序字段,就是DB表的字段的规则,只能是:英文数字下划线。非法的SQL运算符号并不符合这个规则,于是可以考虑通过正则来判断排序字段是否合法。

先假定客户提交动态排序参数的格式

@RequestParam(value = "columns", defaultValue = "createDate") String[] columns,
@RequestParam(value = "orders", defaultValue = "desc") String[] orders

例如:id升序,name逆序,那么检索参数就是: /foo?columns=id,name&orders=asc,desc
多个参数,和多个排序策略使用逗号分隔。一一对应。

PageUtils

封装一个方法,通过排序字段和排序策略,生成排序SQL语句。在方法中完成对字段,排序策略合法性的检查。

public class PageUtils {
	
	static final String DEFAULT_ORDER = "ASC";
	
	public static String order(String[] columns, String[] orders) {

		if (columns == null || columns.length == 0) {
			return "";
		}

		StringBuilder stringBuilder = new StringBuilder();

		for (int x = 0; x < columns.length; x++) {

			String column = columns[x];
			String order = null;
			
			if (orders != null && orders.length > x) {
				order = orders[x].toUpperCase();
				if (!(order.equals("ASC") || order.equals("DESC"))) {
					throw new IllegalArgumentException("非法的排序策略:" + column);
				}
			}else {
				order = DEFAULT_ORDER;
			}

			// 判断列名称的合法性,防止SQL注入。只能是【字母,数字,下划线】
			if (!column.matches("[A-Za-z0-9_]+")) {
				throw new IllegalArgumentException("非法的排序字段名称:" + column);
			}

			// 驼峰转换为下划线
			column = humpConversionUnderscore(column);
			
			if (x != 0) {
				stringBuilder.append(", ");
			}
			stringBuilder.append("`" + column + "` " + order);
		}
		return stringBuilder.toString();
	}

	public static String humpConversionUnderscore(String value) {
		StringBuilder stringBuilder = new StringBuilder();
		char[] chars = value.toCharArray();
		for (char charactor : chars) {
			if (Character.isUpperCase(charactor)) {
				stringBuilder.append("_");
				charactor = Character.toLowerCase(charactor);
			}
			stringBuilder.append(charactor);
		}
		return stringBuilder.toString();
	}
}

演示

String orderSql = PageUtils.order(new String[] {"id", "name"}, new String[] {"ASC", "DESC"});
PageHelper.offsetPage(1, 10).setOrderBy(orderSql);

List<FooEntity> foos = this.fooMapper.foos();
foos.forEach(System.out::println);
// SQL语句:String orderSql = PageUtils.order(new String[] {"id", "name"}, new String[] {"ASC", "DESC"});
PageHelper.offsetPage(1, 10).setOrderBy(orderSql);

List<FooEntity> foos = this.fooMapper.foos();
foos.forEach(System.out::println);

客户端动态排序显得非常灵活,并且没有SQL注入的风险。最多也就是客户端恶意提交排序的字段,在检索结果集中找不到,导致SQL异常。但是不会导致执行危险的SQL语句。