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 | 105 | <groupId>com.fhs-opensource</groupId> |
| 106 | 106 | <artifactId>easy-trans-mybatis-plus-extend</artifactId> |
| 107 | 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 | 115 | </dependencies> |
| 109 | 116 | |
| 110 | 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 | 23 | import java.util.ArrayList; |
| 24 | 24 | import java.util.Collection; |
| 25 | 25 | import java.util.List; |
| 26 | +import java.util.regex.Pattern; | |
| 26 | 27 | |
| 27 | 28 | /** |
| 28 | 29 | * MyBatis 工具类 |
| ... | ... | @@ -31,6 +32,8 @@ public class MyBatisUtils { |
| 31 | 32 | |
| 32 | 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 | 37 | public static <T> Page<T> buildPage(PageParam pageParam) { |
| 35 | 38 | return buildPage(pageParam, null); |
| 36 | 39 | } |
| ... | ... | @@ -42,8 +45,11 @@ public class MyBatisUtils { |
| 42 | 45 | // 排序字段 |
| 43 | 46 | if (CollUtil.isNotEmpty(sortingFields)) { |
| 44 | 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 | 55 | return page; |
| ... | ... | @@ -57,23 +63,29 @@ public class MyBatisUtils { |
| 57 | 63 | if (wrapper instanceof QueryWrapper<T>) { |
| 58 | 64 | QueryWrapper<T> query = (QueryWrapper<T>) wrapper; |
| 59 | 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 | 72 | } else if (wrapper instanceof LambdaQueryWrapper<T>) { |
| 65 | 73 | // LambdaQueryWrapper 不直接支持字符串字段排序,使用 last 方法拼接 ORDER BY |
| 66 | 74 | LambdaQueryWrapper<T> lambdaQuery = (LambdaQueryWrapper<T>) wrapper; |
| 67 | 75 | StringBuilder orderBy = new StringBuilder(); |
| 68 | 76 | for (SortingField sortingField : sortingFields) { |
| 77 | + String columnName = buildSafeOrderColumn(sortingField.getField()); | |
| 78 | + if (columnName == null) { | |
| 79 | + continue; | |
| 80 | + } | |
| 69 | 81 | if (StrUtil.isNotEmpty(orderBy)) { |
| 70 | 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 | 89 | // 另外个思路:https://blog.csdn.net/m0_59084856/article/details/138450913 |
| 78 | 90 | } else { |
| 79 | 91 | throw new IllegalArgumentException("Unsupported wrapper type: " + wrapper.getClass().getName()); |
| ... | ... | @@ -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 | 114 | * 由于 MybatisPlusInterceptor 不支持添加拦截器,所以只能全量设置 | ... | ... |
yudao-module-mall/yudao-module-trade/src/main/resources/mapper/brokerage/BrokerageUserMapper.xml
| ... | ... | @@ -2,9 +2,19 @@ |
| 2 | 2 | <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> |
| 3 | 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 | 16 | <select id="selectSummaryPageByUserId" |
| 6 | 17 | resultType="cn.iocoder.yudao.module.trade.controller.app.brokerage.vo.user.AppBrokerageUserChildSummaryRespVO"> |
| 7 | - | |
| 8 | 18 | SELECT bu.id, bu.bind_user_time AS brokerageTime, |
| 9 | 19 | (SELECT SUM(price) FROM trade_brokerage_record r |
| 10 | 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 | 33 | </if> |
| 24 | 34 | </where> |
| 25 | 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 | 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 | 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 | 47 | </when> |
| 35 | 48 | <otherwise> |
| 36 | 49 | ORDER BY bu.bind_user_time DESC | ... | ... |