Explorar el Código

fix 修复兑换码详情

zoujiajian hace 9 horas
padre
commit
177b8cb258

+ 8 - 0
docs/exchange-code-security.md

@@ -41,3 +41,11 @@
 ```powershell
 & 'D:\IntelliJ IDEA 2025.2.3\plugins\maven\lib\maven3\bin\mvn.cmd' -pl netflix-web -am test '-Dtest=ExchangeCodeSecurityTest,CouponWriteSecurityTest' '-Dsurefire.failIfNoSpecifiedTests=false'
 ```
+
+
+## 2026-10-21 用户端兑换码详情加固
+- `GET /applets/user/get/ticket/code/detail` 只接受完整 code,缺失、空串、空白、非法字符和超过 256 字符均拒绝。兼容字母数字、下划线和连字符。
+- 使用框架一次 URL 解码后的值,不再二次解码。改用 MyBatis 参数绑定的 code 等值查询,额外 skuIds/useStatus/排序/操作符不参与查询。
+- 返回前用 Java 严格比较原码,避免数据库不区分大小写的匹配;响应不再包含 code 字段。前端应使用自己的输入码执行兑换,不依赖详情响应回填。
+- 不修改兑换核销流程,不增加数据库迁移。此改动未新增限流,不代表全部兑换安全问题已完成。
+- 部署验收:空码与错误码不返回数据;完整有效测试码正常展示;附加参数不能改变查询目标;响应不含完整兑换码。

+ 2 - 0
netflix-dao/src/main/java/com/cyksj/model/views/ExchangeCodeDetailView.java

@@ -3,6 +3,7 @@ package com.cyksj.model.views;
 import com.ejlchina.searcher.bean.DbField;
 import com.ejlchina.searcher.bean.DbIgnore;
 import com.ejlchina.searcher.bean.SearchBean;
+import com.fasterxml.jackson.annotation.JsonIgnore;
 import lombok.Getter;
 import lombok.Setter;
 
@@ -19,6 +20,7 @@ import java.util.List;
 @Setter
 @SearchBean(tables = "exchange_code ec")
 public class ExchangeCodeDetailView {
+	@JsonIgnore
 	@DbField("ec.code")
 	private String code;
 

+ 21 - 9
netflix-web/src/main/java/com/cyksj/web/controller/user/UserController.java

@@ -3,7 +3,6 @@ package com.cyksj.web.controller.user;
 import cn.hutool.core.date.DateTime;
 import cn.hutool.core.date.DateUtil;
 import cn.hutool.core.util.StrUtil;
-import cn.hutool.core.util.URLUtil;
 import cn.hutool.extra.servlet.ServletUtil;
 import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import com.cyksj.common.annotation.NoSubmit;
@@ -14,6 +13,7 @@ import com.cyksj.enums.GatewayApiCode;
 import com.cyksj.enums.GatewayResponse;
 import com.cyksj.mapper.*;
 import com.cyksj.mapper.manage.coupon.CouponUserMapper;
+import com.cyksj.mapper.manage.exchange.ExchangeCodeMapper;
 import com.cyksj.mapper.manage.distribute.*;
 import com.cyksj.mapper.mirrorshop.UserMirrorShopMapper;
 import com.cyksj.model.dto.UserWhoami;
@@ -62,6 +62,8 @@ public class UserController {
 
     private final ExchangeCodeService exchangeCodeService;
 
+    private final ExchangeCodeMapper exchangeCodeMapper;
+
     private final CouponUserMapper couponUserMapper;
 
     private final OrderDonMapper orderDonMapper;
@@ -128,18 +130,28 @@ public class UserController {
      * 车票兑换码详情
      */
     @GetMapping("/get/ticket/code/detail")
-    public Result<ExchangeCodeDetailView> getTicketCodeDetail(String code) throws Exception {
-        if (StrUtil.isBlank(code) || code.length() > 256) {
+    public Result<ExchangeCodeDetailView> getTicketCodeDetail(
+            @RequestParam(value = "code", required = false) String code) throws Exception {
+        // Spring already decodes query parameters. Never decode a second time.
+        // Accept legacy underscore/hyphen codes, but reject whitespace and encoded payloads.
+        if (code == null || !code.matches("[A-Za-z0-9_-]{1,256}")) {
             throw BusinessRuntimeException.getInstance("错误兑换码");
         }
-        code = URLUtil.decode(code);
-        ExchangeCodeDetailView exchangeCodeDetailView = beanSearcher.searchFirst(ExchangeCodeDetailView.class, MapUtils.builder().field(ExchangeCodeDetailView::getCode, code).build());
-        if (exchangeCodeDetailView == null) {
+        // Do not pass request parameters to BeanSearcher: all filters are server-owned.
+        ExchangeCode record = exchangeCodeMapper.selectOne(Wrappers.lambdaQuery(ExchangeCode.class)
+                .eq(ExchangeCode::getCode, code).last("LIMIT 1"));
+        // Also reject case-insensitive matches from a *_ci database collation.
+        if (record == null || !code.equals(record.getCode())) {
             throw BusinessRuntimeException.getInstance("错误兑换码");
         }
-        List<Long> skuIds = Jsons.parseList(exchangeCodeDetailView.getSkuIds(), Long.class);
-        exchangeCodeDetailView.setSkuDetails(skuMapper.selectExchangeTitleSkus(skuIds));
-        return GatewayResponse.SUCCESS.newBuilder().toResult(exchangeCodeDetailView);
+        ExchangeCodeDetailView detail = new ExchangeCodeDetailView();
+        detail.setSkuIds(record.getSkuIds());
+        detail.setUseStatus(record.getUseStatus());
+        detail.setUseTime(record.getUseTime());
+        detail.setCodeType(record.getCodeType());
+        List<Long> skuIds = Jsons.parseList(record.getSkuIds(), Long.class);
+        detail.setSkuDetails(skuMapper.selectExchangeTitleSkus(skuIds));
+        return GatewayResponse.SUCCESS.newBuilder().toResult(detail);
     }
 
     /**