← Back to blog
Intigriti CTF · October 2026

Coupon stacking lets checkout drop a €666 item to €0

Two ordinary promo codes, one missing server-side bound, and a checkout that treats a fully discounted vault as affordable.

Business logic Promotion stacking Halloween Store · Globules
Midnight Collection€666.00
Payable€0.00
Targetchallenge-1026.challenges.intigriti.io
Cart itemmidnight-vault
Starting credit€66.00

Impact: the Midnight Collection costs €666.00, but stacking the available promotions reduces the total to €0.00. Checkout accepts the order against the shop's €66.00 store credit and reveals the flag on the receipt.

The setup

The Halloween Store starts with €66.00 in store credit. Checkout is allowed when the cart total is less than or equal to the available credit. Two public promotions are available:

CodeSourceDiscount
NEWS30Newsletter signup (POST /api/newsletter)30%
NIGHT20Site banner20%

The cart API tracks a count for each code and adds percentage × count to the discount. The combined discount is not capped at 100%, and the one-use limit is not consistently enforced.

From €666 to €0

Apply each promotion twice: NEWS30 contributes 60%, and NIGHT20 contributes 40%.

PromotionCalculationDiscount
NEWS30€666 × 30% × 2−€399.60
NIGHT20€666 × 20% × 2−€266.40
Total discount€399.60 + €266.40−€666.00

The payable total becomes €0.00, so it passes the €66.00-credit checkout check.

The Halloween Store's NEWS30 coupon
NEWS30 advertises a 30% discount.
The Halloween Store's NIGHT20 coupon
NIGHT20 adds another 20% discount.

The “already applied” response

A repeated POST /api/cart/coupon with {"code":"NEWS30"} can return HTTP 409:

HTTP/1.1 409 Conflict
Content-Type: application/json

{"ok":false,"error":"coupon_already_applied","message":"Coupon already applied."}

That response did not reliably prevent the later cart from showing the repeated applications. The observed cart showed:

  • NEWS30: 30% × 2 = €399.60 off
  • NIGHT20: 20% × 2 = €266.40 off
  • Subtotal €666.00; complimentary delivery; total €0.00
  • Available store credit €66.00

Reproduction

Keep the same session cookie across all requests:

  1. Add the item with POST /api/cart/items and body {"productId":"midnight-vault"}.
  2. Apply NEWS30 twice with POST /api/cart/coupon and body {"code":"NEWS30"}.
  3. Apply NIGHT20 twice with POST /api/cart/coupon and body {"code":"NIGHT20"}.
  4. Check GET /cart for both promotions at count 2 and a €0.00 total.
  5. Submit POST /api/checkout with body {}.
Checkout confirmation for the fully discounted cart
The €0.00 cart passes checkout despite the €66.00 credit balance.

Checkout succeeds. The confirmation modal shows the flag under “Inside the archive”:

The challenge flag shown in the checkout receipt
The successful order receipt reveals the challenge flag.

INTIGRITI{094638e6-ba2f-4c50-8bb4-41ced0d5debd}

Root cause

The discount calculation effectively subtracts price × (percent / 100) × count for each promotion. There is no upper bound on the accumulated discount, and the count can exceed one even when the client receives an “already applied” response.

Checkout then trusts the calculated cart total and checks only whether it is within the available credit. The €666 item therefore passes with a €0.00 payable total and €66.00 in credit.

What should be fixed

  • Enforce coupon eligibility and usage limits on the server, atomically with the cart update.
  • Recalculate discounts from validated promotion records rather than trusting a client-visible count.
  • Cap the combined discount at the allowed maximum, normally the eligible subtotal.
  • Recompute and validate the final payable amount at checkout using integer minor currency units.

A rejected response is not a security control unless the server also preserves the invariant in its state and enforces it again at checkout.