【海外SRC】我是如何“清空”全网用户购物车的?

不知道大家有没有这种感觉:哪怕已经在安全圈摸爬滚打了好些年,但每当在深夜的宿舍里,伴随着机械键盘的敲击声,抓到一个高危 0-day 的那一刻,那种肾上腺素飙升的快感,依然和大学时代第一次弹窗 alert(1) 时一样纯粹且热烈。

今天,我想和大家分享一个近期在某国际知名电商平台(下文统称 TargetShop)挖掘到的高危 API 漏洞。这个漏洞没有眼花缭乱的内存溢出,也没有复杂的 RCE 链,它依靠的仅仅是安全工程师最基础的直觉。

0x01 信息搜集与直觉

一切起源于日常的 Web 资产测绘。

当时我正在对 TargetShop 的 Web 应用层进行被动抓包分析。像往常一样,我打开了Burpsuite进行抓包,模拟普通用户的正常购物流程:浏览商品 -> 加入购物车 -> 查看购物车。

在繁杂的请求流中,我的目光锁定在了一个调用电商核心框架(Storefront API)的接口上。它的根路径暴露得很明显,且 URL 结构引起了我的怀疑:

https://api.example-shop.com/v1/baskets/{basketId}

根据以往经验,看到 {basketId} 这种直接暴露在 RESTful URL 路径中的标识符,我的大脑立刻响起了警报。
1.参数化特征: 这是一个典型的资源请求接口。
2.直觉拷问: 这个 basketId 是怎么生成的?是强随机的 UUID(比如 550e8400-e29b-41d4-a716-446655440000),还是简单的自增数字序列(Sequential ID)?后端在处理这个请求时,有没有校验“当前请求的用户凭证”与“该购物车ID的所有者”是否匹配?

没有犹豫,我决定把这个请求送进 Burp Suite 的 Repeater 模块,看看它到底藏着什么猫腻。

0x02 详细步骤拆解

漏洞利用的核心,往往在于“逻辑转换”的瞬间——从遵守规则的正常请求,转变为试探底线的恶意 Payload。

阶段一:未授权读取他人购物车

起初,我的正常请求是带着我自己的 basketId 的。接着,我做了一个最简单的动作:把 URL 中的 ID 替换成了一个随机的整数 1000。

构造的 Payload 请求包如下:

GET /v1/baskets/1000 HTTP/1.1
Host: api.example-shop.com
User-Agent: curl/8.10.1
Accept: */*
# 注意:这里我甚至没有携带任何认证 Token 或有效的 Session Cookie!

本以为后端会冰冷地甩给我一个 401 Unauthorized 或 403 Forbidden,然而,接下来的发现推翻了我的假设。服务器竟然返回了 200 OK,并且吐出了极其详尽的 JSON 数据!

响应包高亮拆解:

HTTP/1.1 200 OK
Date: Tue, 02 Jun 2026 07:35:22 GMT
Content-Type: application/json; charset=utf-8
...[省略部分响应头]...

{
  "key": "1000",
  "items": [
    {
      "key": "600a5dcd493a934dc9a3ec52b375ca6d",
      "packageId": 1,
      "quantity": 1,
      "status": "available",
      "displayData": {},
      "availableQuantity": 999,
      "customData": {},
      "lowestPriorPrice": ...
      // 变体ID: 99999999 (脱敏), 价格: 34.99 欧元
      // 物流信息: 某国际快递,预计 6月3-5日送达
    }
  ]
}

一个教科书级别的 IDOR(Insecure Direct Object Reference,不安全的直接对象引用),或者现在更准确的叫法:BOLA(Broken Object Level Authorization) 漏洞确认存在!通过遍历 basketId,我可以直接读取任意用户的购物车商品明细、商品变体 ID、价格甚至是快递物流预期信息

阶段二:越权操作——删除他人商品
如果只能“看”,那这个漏洞的危害顶多是信息泄露。但黑客的思维永远是得寸进尺的。既然 GET 方法没有做权限隔离,那么 DELETE 呢?
我注意到响应包里的商品实体有一个唯一标识符(items[0].key:600a…ca6d)。于是,我立刻构造了一个删除他人购物车商品的请求:


越权删除的请求包:

DELETE /v1/baskets/1000/items/600a5dcd493a934dc9a3ec52b375ca6d HTTP/1.1
Host: api.example-shop.com
User-Agent: curl/8.10.1
Accept: */*

响应结果显示,目标用户的购物车已经被彻底清空(items 变为 [])。为了不影响真实用户,我在测试后立刻利用 POST 接口将商品恢复,但这已经足以证明该漏洞的毁灭性。

0x03 震撼的破坏力

不要小看这个简单的越权,让我们把视角拉高,看看它在真实商业场景下的破坏力:


1.大促期间的商业狙击: 假设在“双十一”或“黑五”抢购高峰期,攻击者只需编写一个简单的 Python 脚本,开多线程遍历 basketId 并发送 DELETE 请求。这能在几分钟内清空数百万用户的购物车,直接阻断平台的交易链路,造成不可估量的资金损失。


2.核心业务数据泄露: 根据我的进一步深挖,该 API 域名(api.example-shop.com)关联的后端服务覆盖了该集团下所有的区域站点(如 shopId: 999)。更可怕的是,攻击者不仅能获取用户数据,甚至能通过接口遍历出长达 16867 行的完整 Swagger API 规范文档、未公开的商店配置以及内部营销活动策略。


3.竞品分析的“天眼”: 竞争对手完全可以通过监听这些数据,精准掌握该平台当前的热销商品走势和用户消费偏好。

0x04 防御与反思

作为渗透测试者,我们享受攻破的快感;但作为安全工程师,我们需要回归建设,思考问题产生的根源。

这个漏洞的本质在于“鉴权层与业务逻辑层的脱节”。开发者可能在网关层做了一般的 API 路由校验,却在具体的对象获取逻辑(Controller/Service)中,完全信任了用户传入的参数,而没有去比对这个参数的所有权。

如何彻底修复此类漏洞?给学弟学妹和开发大佬们的建议:
1.废弃可预测的连续 ID (不可预测性):不要使用自增数字(如 1000, 1001)作为 basketId。应当全面改用 UUID v4(通用唯一识别码)。虽然 UUID 不能替代鉴权,但它能极大增加遍历攻击的成本。
2.强制实施对象级别访问控制 (Object-Level Access Control):在处理任何 CRUD(创建、读取、更新、删除)操作前,必须在代码逻辑中加入所有权校验。

伪代码示例:

String currentUserId = jwt.getUserId();
Basket basket = basketRepository.findById(requestBasketId);
if (!basket.getOwnerId().equals(currentUserId)) {
    throw new UnauthorizedException("You do not have permission to access this cart.");
}

3.零信任架构: API 接口不应该假设“只有正常的客户端才会发起请求”。每一个请求,哪怕是内部微服务间的调用,都必须经过严格的身份验证(Authentication)和授权验证(Authorization)。

安全之路,道阻且长。这次的购物车 API 惊魂记再次印证了一个道理:在复杂的系统架构中,最致命的往往不是高深莫测的技术,而是对基础权限控制的轻视。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注