解决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语句。