Commit cab59d4dd8fd1644f3b5137e2b851417f055d9fc
1 parent
ff8a5241
fix(framework): 修复分页排序 SQL 注入风险
Showing
3 changed files
with
64 additions
and
16 deletions
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 |