CVE-2026-110121, CVE-2026-110122: lilishop卖家API允许任意卖家通过商品编辑端点覆盖并接管其他店铺的商品(跨店授权绕过) 漏洞概述 lilishop是一个多商户电商平台,每个卖家使用店铺范围的令牌进行登录,并被期望仅管理属于自己店铺的商品。卖家商品编辑端点 未强制执行此约束。控制器将路径变量 直接传递给 ,该函数从请求体构建 实体,将其主键设置为调用者提供的 ,执行 ,然后调用 。 内部唯一的检查是 ,它按ID加载行,如果缺失则抛出异常。它从未将该行的 与当前卖家进行比较。由于 也会在更新前覆盖 ,卖家可以编辑另一个卖家的 ,覆盖该商品的内容并将其重新分配给攻击者的店铺。 同一模块中显示了意图保护紧邻易受攻击方法的守卫机制。 (上下架)和 均通过 构建它们的更新操作,该函数会在 语句中添加 条件,因此卖家只能更改自己店铺内的行。 及其同级方法 跳过了该辅助函数,因此它们的写入操作不受店铺范围限制。系统中没有全局的MyBatis-Plus租户或数据权限拦截器来补偿这一缺陷,且卖家安全配置仅要求经过身份验证的 令牌,缺乏每店铺的数据范围限制。单个有效的卖家账户,且指向一个已在店铺中公开的商品,足以在平台上篡改或夺取任何商品。 影响范围 项目: lilishop/lilishop, Spring Cloud B2BC2多商户商城, seller-api 商品管理 受影响版本: v4.2.4 (固定提交 9f16c27ef5a1430738c742eafae18a797f833dc32)。相同的代码路径存在于 v4.2.5, v4.3, v4.4 以及当前的 master 分支。 类: CWE-639 通过用户控制键绕过授权, CWE-862 缺少授权, CWE-863 不正确的授权 状态: 协调披露进行中。尚未分配CVE。 修复方案 当前模式: 在同一类中, 构建了一个绑定到当前店铺的正确更新包装器。 上架和下架: 逻辑删除操作均使用了该包装器,因此卖家无法通过那些端点更改自己店铺之外的商品。 editGoods: 使用 ,没有店铺谓词。第二个受影响的方法 构建了一个按ID范围更新的包装器。 freight: 验证运费模板属于调用者,然后将其模板ID应用到任何 上。调用者命名了这些ID。卖家可以将自己的运费模板绑定到另一个卖家的商品上,从而改变这些商品的运费成本模型。 全局安全网: 没有全局安全网。唯一的MyBatis-Plus拦截器注册是分页,没有租户或数据权限过滤器来重写这些语句。 详细攻击步骤 1. 登录并获取店铺令牌: 卖家登录路径为 ,返回一个 。将其放置在请求头中,过滤器读取 。 2. 选择受害者商品ID: 打开属于另一个店铺的商品,从买家商品API获取 。 3. 构建更新请求: 使用攻击者的店铺令牌和受害者商品ID构建更新请求。 4. 发送请求: 发送更新请求,覆盖受害者商品并将其重新分配给攻击者的店铺。 java // GoodsStoreController.java:109-114 @ApiOperation(value = "修改商品") @PutMapping(value = "/update/{goodsId}", consumes = "application/json", produces = "application/json") public Result update(@RequestBody GoodsOperationDTO goodsOperationDTO, @PathVariable String goodsId) { return ResultUtil.success(goodsService.editGoods(goodsOperationDTO, goodsId)); } java // GoodsServiceImpl.java:181-196 public void editGoods(GoodsOperationDTO goodsOperationDTO, String goodsId) { Goods goods = new Goods(goodsOperationDTO); goods.setGoodsId(goodsId); // 检查商品信息 this.checkGoods(goods); // 修改商品 this.updateById(goods); // 修改商品sku信息 this.goodsSkuService.update(goodsOperationDTO.getSkuList(), goods, goodsOperationDTO.getGoodsId()); } // GoodsServiceImpl.java:481-491 private Goods checkGoods(Goods goods) { // 检查商品是否存在 if (goods.getId() == null) { log.error("商品id为" + goodsId + "的商品不存在"); throw new ServiceException(ResultCode.GOODS_NOT_EXIST); } return goods; } java // StoreSecurityConfig.java:62-77 .authorizeRequests() .anyRequest() .authenticated() .and() .addFilter(new StoreAuthenticationFilter(authenticationManager()), CsrfToken.class); // StoreAuthenticationFilter.java:85-98 if (cache.hasKey(CachePrefix.ACCESS_TOKEN.getPrefix(userEnums.STORE) + jwt)) { List auths = new ArrayList(); auths.add(new SimpleGrantedAuthority("ROLE_" + authUser.getRole().name())); } java // GoodsServiceImpl.java:561-567 private LambdaUpdateWrapper getUpdateWrapperByStoreAuthority() { LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper(); AuthUser authUser = this.checkStoreAuthority(); if (authUser != null) { updateWrapper.eq(Goods::getStoreId, authUser.getStoreId()); } return updateWrapper; } java // GoodsServiceImpl.java:291-308 LambdaUpdateWrapper updateWrapper = this.getUpdateWrapperByStoreAuthority(); updateWrapper.set(Goods::getMarketEnable, goodsStatusEnum.name()); updateWrapper.set(Goods::getUpdateTime, new Date()); updateWrapper.in(Goods::getId, goodsIds); java // GoodsServiceImpl.java:353-358 LambdaUpdateWrapper updateWrapper = this.getUpdateWrapperByStoreAuthority(); updateWrapper.set(Goods::getDeleteFlag, GoodsStatusEnum.DOWN.name()); updateWrapper.in(Goods::getId, goodsIds); java // GoodsServiceImpl.java:377-392 public Boolean freight(List goodsIds, String templateId) { FreightTemplate freightTemplate = freightTemplateService.getById(templateId); if (freightTemplate == null) { throw new ServiceException(ResultCode.FREIGHT_TEMPLATE_NOT_EXIST); } if (authUser != null && !freightTemplate.getStoreId().equals(authUser.getStoreId())) { throw new ServiceException(ResultCode.USER_AUTHORITY_ERROR); } LambdaUpdateWrapper lambdaUpdateWrapper = Wrappers.lambdaUpdate(); lambdaUpdateWrapper.set(Goods::getTemplateId, templateId); lambdaUpdateWrapper.in(Goods::getId, goodsIds); return this.update(lambdaUpdateWrapper); } java // MyBatisPlusConfig.java:19-24 @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; }