在电商网站的商品购买流程中,负数量与价格篡改是两种典型的业务逻辑漏洞与安全威胁。负数量攻击指用户通过修改购物车或订单提交时的商品数量参数为负数(如-1),导致系统计算出错误的总价,甚至可能触发“反向支付”或积分、余额异常增加。价格篡改则指用户在提交订单前,通过前端传参或网络抓包,直接修改商品单价、运费或折扣金额,从而以远低于正常售价的价格完成交易。这两种问题不仅直接造成商家的经济损失,更暴露了网站在服务器端校验、业务规则完整性与数据一致性验证上的严重缺失。

一、 负数量攻击的原理与常见场景

负数量攻击的核心在于,网站在计算订单总价时,过于依赖前端(如浏览器、APP)提交的数据,而未在服务器后端进行严格的业务逻辑校验。一个典型的漏洞场景是:购物车商品单价为100元,用户通过浏览器开发者工具或抓包软件,将提交的购买数量参数从“1”修改为“-1”。如果后端代码简单地用“单价 × 数量”来计算商品小计,结果将是-100元。当这个负值被累加到订单总价时,就可能出现总价为负数的情况。

更危险的情况发生在涉及积分、优惠券或余额抵扣的复杂业务中。例如,某些系统设计为:当订单总价为负时,不仅不向用户收款,反而会向用户的账户余额或积分账户中增加对应金额。攻击者通过提交包含大量负数量商品的订单,可能实现“零元购”甚至“套现”。另一种衍生场景是“数量为零”或“极小数量”(如0.0001)的攻击,结合某些系统的四舍五入规则,也可能导致支付金额为零。

二、 价格篡改漏洞的渗透路径

价格篡改通常比负数量攻击更直接。攻击者在前端页面点击“立即购买”或“提交订单”时,拦截并修改发送至服务器的网络请求。常见的可篡改参数包括:productPrice(商品单价)、totalAmount(订单总额)、shippingFee(运费)、discount(折扣额)等。如果服务器没有将这些关键价格参数与数据库中的标准价格进行二次比对,而是直接信任前端传入的值,攻击者就能以任意价格下单。

这类漏洞常出现在拍卖、秒杀、团购等价格动态变化的场景中,也常见于企业级B2B网站,其中商品价格可能因客户协议而不同。如果权限校验不严,攻击者可能通过修改“priceId”或“contractId”等参数,越权匹配到更低的价格方案。此外,在移动端APP中,若未对通信进行有效加固(如证书绑定、参数签名),篡改难度会更低。

三、 根本原因:缺乏服务器端校验与业务规则闭环

无论是负数量还是价格篡改,其根源都在于“信任了不可信的前端输入”。许多开发团队错误地认为,前端通过JavaScript禁用负数输入、隐藏价格字段或进行校验就已足够。然而,所有前端验证都只能提升用户体验,无法提供任何安全保证。攻击者可以完全绕过浏览器界面,直接向服务器API发送构造好的恶意请求。

更深层次的原因在于业务规则没有在服务层形成闭环。一个健壮的购买流程应当:

(1)在收到下单请求时,根据商品ID从服务端数据库重新查询最新单价与库存状态;

(2)根据用户身份与营销规则,在服务端独立计算所有折扣、运费;

(3)对数量、单价、总价等关键数值进行逻辑合理性校验(如数量必须为正整数,单价不能低于成本价,总价不能为负等);

(4)最终生成的支付金额必须完全由服务端计算得出,并作为唯一可信值传递给支付网关。

四、 详细的技术解决方案与代码示例

解决这些问题需要在后端(如Java Spring Boot、Python Django、Node.js等框架)的关键业务节点植入严格的校验逻辑。以下是核心防御策略:

1. 商品数量校验:

在订单创建的服务方法中,必须强制校验购买数量。

// Java Spring Boot 示例
public ResponseEntity<Order> createOrder(@RequestBody OrderRequest request) {
    // 从请求中获取商品ID和数量
    Long productId = request.getProductId();
    Integer quantity = request.getQuantity();

    // 校验1:数量必须为正整数
    if (quantity == null || quantity <= 0) {
        throw new IllegalArgumentException("购买数量必须为正整数");
    }

    // 校验2:数量是否超过合理上限(防溢出)
    if (quantity > 1000) {
        throw new IllegalArgumentException("单次购买数量超过上限");
    }

    // 从数据库查询商品真实信息
    Product product = productRepository.findById(productId)
        .orElseThrow(() -> new ResourceNotFoundException("商品不存在"));

    // 校验3:库存检查
    if (product.getStock() < quantity) {
        throw new BusinessException("库存不足");
    }

    // 计算金额:始终使用服务端查询到的单价
    BigDecimal unitPrice = product.getCurrentPrice(); // 从数据库获取
    BigDecimal itemTotal = unitPrice.multiply(new BigDecimal(quantity));

    // ... 后续订单处理逻辑
}

2. 价格重新计算与比对:

绝不可信任前端传来的任何价格相关参数。应在服务端根据商品ID、用户等级、活动规则独立计算所有金额。

// Node.js (Express) 示例
app.post('/api/order', async (req, res) => {
    const { items, couponCode } = req.body; // items: [{productId, quantity}]

    let total = 0;
    const orderItems = [];

    for (const item of items) {
        // 重新查询商品信息
        const product = await Product.findByPk(item.productId);
        if (!product) {
            return res.status(400).json({ error: '商品不存在' });
        }

        // 使用数据库中的单价
        const unitPrice = product.price;
        const quantity = parseInt(item.quantity);
        const subTotal = unitPrice * quantity;

        // 关键:比对前端传来的小计(如果传了)与服务端计算的小计
        // 通常建议根本不接收前端小计,若因业务需要接收,则必须严格校验
        if (item.subTotal && Math.abs(item.subTotal - subTotal) > 0.01) {
            return res.status(400).json({ error: '价格校验失败' });
        }

        total += subTotal;
        orderItems.push({ productId: product.id, quantity, unitPrice, subTotal });
    }

    // 服务端独立计算运费、折扣
    const shippingFee = await calculateShippingFee(req.user.id, total);
    const discount = await calculateDiscount(req.user.id, couponCode, total);
    const finalAmount = total + shippingFee - discount;

    // 再次校验:最终金额是否合理(例如不能为负或过低)
    if (finalAmount < 0) {
        return res.status(400).json({ error: '订单金额异常' });
    }
    if (finalAmount > 0 && finalAmount < product.getMinOrderAmount()) {
        return res.status(400).json({ error: '未达到最小订单金额' });
    }

    // 创建订单,存储服务端计算出的金额
    const order = await Order.create({
        userId: req.user.id,
        totalAmount: total,
        shippingFee,
        discount,
        finalAmount,
        status: 'pending'
    });

    res.json({ orderId: order.id, amount: finalAmount });
});

3. 关键参数签名与防重放攻击:

对于高安全要求的场景,可采用参数签名机制。服务器在用户浏览商品时生成一个令牌(Token),包含商品ID、单价、时间戳的哈希签名。提交订单时,必须附带此令牌,服务器验证令牌有效性且未被使用过,确保价格未被中途篡改。

五、 业务层与数据层的补充防护

除了代码层面的校验,还需在架构和流程上加固:

1. 数据库约束: 在订单明细表(order_items)中,设置 "quantity" 字段为无符号整数(UNSIGNED),从数据库层面杜绝负数存储。同时,存储商品快照信息,包括下单时的单价(snapshot_price),用于后续审计与争议处理。

2. 支付前最终复核: 在调用支付网关前,应再次从数据库拉取订单完整信息,复核最终支付金额。支付回调成功后,应标记订单为“已支付”,并立即减少库存,避免并发问题导致超卖。

3. 监控与告警: 建立异常订单监控系统。对以下行为触发实时告警:订单总价为负或零、单价低于成本价一定比例、同一用户短时间下单频率异常、使用异常折扣组合等。这些日志应关联用户ID和IP,便于追踪与封禁恶意账户。

4. 定期安全审计与渗透测试: 定期对购买流程进行黑盒与白盒测试,模拟攻击者的篡改行为。审计所有涉及金额计算的API接口,确保没有遗漏的校验点。特别关注移动端API、第三方对接接口等容易被忽视的入口。

六、 总结:构建纵深防御体系

彻底杜绝网站商品购买中的负数量与价格篡改问题,不能依赖单一措施。必须构建一个从前端交互、网络传输、服务端逻辑到数据存储的纵深防御体系。核心原则始终是:“前端仅为展示与交互,所有业务规则与关键决策必须在可信任的服务端执行。” 开发者需牢固树立“永不信任客户端输入”的安全意识,在每一个价格与数量参数流入核心业务前,都进行严格的重新查询、逻辑校验与一致性比对。同时,结合监控、审计等运营手段,才能形成一个弹性、健壮的电商交易系统,在提升用户体验的同时,保障商家与平台的资金安全。