Commit cab59d4dd8fd1644f3b5137e2b851417f055d9fc

Authored by YunaiV
1 parent ff8a5241

fix(framework): 修复分页排序 SQL 注入风险

yudao-framework/yudao-spring-boot-starter-mybatis/pom.xml
@@ -105,6 +105,13 @@ @@ -105,6 +105,13 @@
105 <groupId>com.fhs-opensource</groupId> 105 <groupId>com.fhs-opensource</groupId>
106 <artifactId>easy-trans-mybatis-plus-extend</artifactId> 106 <artifactId>easy-trans-mybatis-plus-extend</artifactId>
107 </dependency> 107 </dependency>
  108 +
  109 + <!-- Test 测试相关 -->
  110 + <dependency>
  111 + <groupId>org.springframework.boot</groupId>
  112 + <artifactId>spring-boot-starter-test</artifactId>
  113 + <scope>test</scope>
  114 + </dependency>
108 </dependencies> 115 </dependencies>
109 116
110 </project> 117 </project>
yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/util/MyBatisUtils.java
@@ -23,6 +23,7 @@ import net.sf.jsqlparser.schema.Table; @@ -23,6 +23,7 @@ import net.sf.jsqlparser.schema.Table;
23 import java.util.ArrayList; 23 import java.util.ArrayList;
24 import java.util.Collection; 24 import java.util.Collection;
25 import java.util.List; 25 import java.util.List;
  26 +import java.util.regex.Pattern;
26 27
27 /** 28 /**
28 * MyBatis 工具类 29 * MyBatis 工具类
@@ -31,6 +32,8 @@ public class MyBatisUtils { @@ -31,6 +32,8 @@ public class MyBatisUtils {
31 32
32 private static final String MYSQL_ESCAPE_CHARACTER = "`"; 33 private static final String MYSQL_ESCAPE_CHARACTER = "`";
33 34
  35 + private static final Pattern SAFE_COLUMN_NAME_PATTERN = Pattern.compile("^[a-zA-Z0-9_]+(\\.[a-zA-Z0-9_]+)*$");
  36 +
34 public static <T> Page<T> buildPage(PageParam pageParam) { 37 public static <T> Page<T> buildPage(PageParam pageParam) {
35 return buildPage(pageParam, null); 38 return buildPage(pageParam, null);
36 } 39 }
@@ -42,8 +45,11 @@ public class MyBatisUtils { @@ -42,8 +45,11 @@ public class MyBatisUtils {
42 // 排序字段 45 // 排序字段
43 if (CollUtil.isNotEmpty(sortingFields)) { 46 if (CollUtil.isNotEmpty(sortingFields)) {
44 for (SortingField sortingField : sortingFields) { 47 for (SortingField sortingField : sortingFields) {
45 - page.addOrder(new OrderItem().setAsc(SortingField.ORDER_ASC.equals(sortingField.getOrder()))  
46 - .setColumn(StrUtil.toUnderlineCase(sortingField.getField()))); 48 + String columnName = buildSafeOrderColumn(sortingField.getField());
  49 + if (columnName == null) {
  50 + continue;
  51 + }
  52 + page.addOrder(new OrderItem().setAsc(isAscOrder(sortingField.getOrder())).setColumn(columnName));
47 } 53 }
48 } 54 }
49 return page; 55 return page;
@@ -57,23 +63,29 @@ public class MyBatisUtils { @@ -57,23 +63,29 @@ public class MyBatisUtils {
57 if (wrapper instanceof QueryWrapper<T>) { 63 if (wrapper instanceof QueryWrapper<T>) {
58 QueryWrapper<T> query = (QueryWrapper<T>) wrapper; 64 QueryWrapper<T> query = (QueryWrapper<T>) wrapper;
59 for (SortingField sortingField : sortingFields) { 65 for (SortingField sortingField : sortingFields) {
60 - query.orderBy(true,  
61 - SortingField.ORDER_ASC.equals(sortingField.getOrder()),  
62 - StrUtil.toUnderlineCase(sortingField.getField())); 66 + String columnName = buildSafeOrderColumn(sortingField.getField());
  67 + if (columnName == null) {
  68 + continue;
  69 + }
  70 + query.orderBy(true, isAscOrder(sortingField.getOrder()), columnName);
63 } 71 }
64 } else if (wrapper instanceof LambdaQueryWrapper<T>) { 72 } else if (wrapper instanceof LambdaQueryWrapper<T>) {
65 // LambdaQueryWrapper 不直接支持字符串字段排序,使用 last 方法拼接 ORDER BY 73 // LambdaQueryWrapper 不直接支持字符串字段排序,使用 last 方法拼接 ORDER BY
66 LambdaQueryWrapper<T> lambdaQuery = (LambdaQueryWrapper<T>) wrapper; 74 LambdaQueryWrapper<T> lambdaQuery = (LambdaQueryWrapper<T>) wrapper;
67 StringBuilder orderBy = new StringBuilder(); 75 StringBuilder orderBy = new StringBuilder();
68 for (SortingField sortingField : sortingFields) { 76 for (SortingField sortingField : sortingFields) {
  77 + String columnName = buildSafeOrderColumn(sortingField.getField());
  78 + if (columnName == null) {
  79 + continue;
  80 + }
69 if (StrUtil.isNotEmpty(orderBy)) { 81 if (StrUtil.isNotEmpty(orderBy)) {
70 orderBy.append(", "); 82 orderBy.append(", ");
71 } 83 }
72 - orderBy.append(StrUtil.toUnderlineCase(sortingField.getField()))  
73 - .append(" ")  
74 - .append(SortingField.ORDER_ASC.equals(sortingField.getOrder()) ? "ASC" : "DESC"); 84 + orderBy.append(columnName).append(" ").append(getOrderDirection(sortingField.getOrder()));
  85 + }
  86 + if (StrUtil.isNotEmpty(orderBy)) {
  87 + lambdaQuery.last("ORDER BY " + orderBy);
75 } 88 }
76 - lambdaQuery.last("ORDER BY " + orderBy);  
77 // 另外个思路:https://blog.csdn.net/m0_59084856/article/details/138450913 89 // 另外个思路:https://blog.csdn.net/m0_59084856/article/details/138450913
78 } else { 90 } else {
79 throw new IllegalArgumentException("Unsupported wrapper type: " + wrapper.getClass().getName()); 91 throw new IllegalArgumentException("Unsupported wrapper type: " + wrapper.getClass().getName());
@@ -81,6 +93,22 @@ public class MyBatisUtils { @@ -81,6 +93,22 @@ public class MyBatisUtils {
81 93
82 } 94 }
83 95
  96 + public static boolean isAscOrder(String order) {
  97 + return SortingField.ORDER_ASC.equals(order);
  98 + }
  99 +
  100 + public static String getOrderDirection(String order) {
  101 + return isAscOrder(order) ? "ASC" : "DESC";
  102 + }
  103 +
  104 + private static String buildSafeOrderColumn(String field) {
  105 + String columnName = StrUtil.toUnderlineCase(field);
  106 + if (StrUtil.isEmpty(columnName) || !SAFE_COLUMN_NAME_PATTERN.matcher(columnName).matches()) {
  107 + return null;
  108 + }
  109 + return columnName;
  110 + }
  111 +
84 /** 112 /**
85 * 将拦截器添加到链中 113 * 将拦截器添加到链中
86 * 由于 MybatisPlusInterceptor 不支持添加拦截器,所以只能全量设置 114 * 由于 MybatisPlusInterceptor 不支持添加拦截器,所以只能全量设置
yudao-module-mall/yudao-module-trade/src/main/resources/mapper/brokerage/BrokerageUserMapper.xml
@@ -2,9 +2,19 @@ @@ -2,9 +2,19 @@
2 <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> 2 <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
3 <mapper namespace="cn.iocoder.yudao.module.trade.dal.mysql.brokerage.BrokerageUserMapper"> 3 <mapper namespace="cn.iocoder.yudao.module.trade.dal.mysql.brokerage.BrokerageUserMapper">
4 4
  5 + <sql id="OrderDirection">
  6 + <choose>
  7 + <when test="@cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils@isAscOrder(sortingField.order)">
  8 + ASC
  9 + </when>
  10 + <otherwise>
  11 + DESC
  12 + </otherwise>
  13 + </choose>
  14 + </sql>
  15 +
5 <select id="selectSummaryPageByUserId" 16 <select id="selectSummaryPageByUserId"
6 resultType="cn.iocoder.yudao.module.trade.controller.app.brokerage.vo.user.AppBrokerageUserChildSummaryRespVO"> 17 resultType="cn.iocoder.yudao.module.trade.controller.app.brokerage.vo.user.AppBrokerageUserChildSummaryRespVO">
7 -  
8 SELECT bu.id, bu.bind_user_time AS brokerageTime, 18 SELECT bu.id, bu.bind_user_time AS brokerageTime,
9 (SELECT SUM(price) FROM trade_brokerage_record r 19 (SELECT SUM(price) FROM trade_brokerage_record r
10 WHERE r.user_id = bu.id AND biz_type = #{bizType} AND r.status = #{status} AND r.deleted = FALSE) AS brokeragePrice, 20 WHERE r.user_id = bu.id AND biz_type = #{bizType} AND r.status = #{status} AND r.deleted = FALSE) AS brokeragePrice,
@@ -23,14 +33,17 @@ @@ -23,14 +33,17 @@
23 </if> 33 </if>
24 </where> 34 </where>
25 <choose> 35 <choose>
26 - <when test="sortingField.field == 'userCount'">  
27 - ORDER BY brokerageUserCount ${sortingField.order} 36 + <when test="sortingField != null and sortingField.field == 'userCount'">
  37 + ORDER BY brokerageUserCount
  38 + <include refid="OrderDirection"/>
28 </when> 39 </when>
29 - <when test="sortingField.field == 'orderCount'">  
30 - ORDER BY brokerageOrderCount ${sortingField.order} 40 + <when test="sortingField != null and sortingField.field == 'orderCount'">
  41 + ORDER BY brokerageOrderCount
  42 + <include refid="OrderDirection"/>
31 </when> 43 </when>
32 - <when test="sortingField.field == 'price'">  
33 - ORDER BY brokeragePrice ${sortingField.order} 44 + <when test="sortingField != null and sortingField.field == 'price'">
  45 + ORDER BY brokeragePrice
  46 + <include refid="OrderDirection"/>
34 </when> 47 </when>
35 <otherwise> 48 <otherwise>
36 ORDER BY bu.bind_user_time DESC 49 ORDER BY bu.bind_user_time DESC