Race condition in coupon application yields $0 checkout totals
POST /api/checkout/complete
description
The checkout completion handler reads the cart total, applies the coupon, deducts inventory, and charges payment as separate non-atomic operations. Issuing multiple concurrent POSTs to /api/checkout/complete with the same cart and coupon causes coupon application to fire more than once per cart. With a 50% off coupon and three concurrent requests, the running total is reduced to a negative value, which is clamped to $0. The order is created, inventory is deducted, and the user is charged nothing.
evidence
Request
POST /api/checkout/complete HTTP/1.1
Host: acme.dev
Authorization: Bearer <session-token>
Content-Type: application/json
{"cart_id":"cart_88a2","coupon":"SAVE50"}
(repeated 5x concurrently)
Response
HTTP/1.1 200 OK
Content-Type: application/json
{
"order_id": "ord_8821",
"total": "$0.00",
"items": [{"sku":"plan_pro","qty":1}],
"charged": "$0.00"
}
Cart subtotal was $98. Coupon SAVE50 was applied four times before the total clamped to zero. Inventory was deducted. The order shipped.
reproduction steps
- Add items to the cart totaling more than $50.
- Apply coupon SAVE50 (50% off, single-use intended).
- Issue 5 concurrent POSTs to /api/checkout/complete with the same cart_id.
- Observe that one or more orders are created with $0 totals.
remediation
Make checkout atomic: lock the cart row, apply the coupon inside the transaction with an idempotency guard on (cart_id, coupon_code), and charge the processor last (reverse the inventory deduction if the charge fails).
sql
BEGIN; SELECT * FROM carts WHERE id = $1 FOR UPDATE; INSERT INTO coupon_uses (cart_id, code) VALUES ($1, $2) ON CONFLICT (cart_id, code) DO NOTHING; -- a second concurrent apply is a no-op -- recompute total, charge the processor, then: COMMIT;
rotate the credentials you provided.
we ran the scan with the credentials you supplied. treat them as compromised and rotate now.
couldn't reproduce this finding?
if it does not reproduce from the evidence above, you get back what this finding cost you: its credits for a credit unlock, or a cash refund for a finding bought through our earlier per-finding checkout. see how claims work, or email team@recon.sh with finding ID f3 and scan ID sample.