GHSA-9rh9-hf3w-9fgg

Suggest an improvement
Source
https://github.com/advisories/GHSA-9rh9-hf3w-9fgg
Import Source
https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/05/GHSA-9rh9-hf3w-9fgg/GHSA-9rh9-hf3w-9fgg.json
JSON Data
https://api.osv.dev/v1/vulns/GHSA-9rh9-hf3w-9fgg
Aliases
Published
2026-05-18T16:37:20Z
Modified
2026-09-10T03:50:46Z
Severity
  • 5.9 (Medium) CVSS_V3 - CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:H/A:N CVSS Calculator
Summary
shopper/framework: Race condition on Discount.usage_limit allows silent over-redemption
Details

Impact

CreateOrderFromCartAction::execute previously created the Order row before checking and incrementing the discount's total_use counter. Under concurrent checkout pressure (Black Friday, flash sale, viral coupon), the global usage_limit was silently exceeded: orders were committed with the discount fully applied to price_amount while the counter blocked at usage_limit. The merchant had no signal that an over-redemption had occurred.

A second related bug: usage_limit_per_user was effectively a no-op because the counter it relied on (DiscountDetail.total_use) was never incremented anywhere in the codebase. The per-user check therefore always saw 0 uses and validation passed regardless of how many times the same customer had previously redeemed the coupon. For eligibility = Everyone the per-user limit could not fire at all because the underlying DiscountDetail row only exists for eligibility = Customers.

Direct financial loss: each over-redemption is a discount the merchant did not intend to grant.

Patches

Fixed in v2.8.0. CreateOrderFromCartAction now:

  • Reserves the discount slot atomically before the order row is created, inside the same DB::transaction with lockForUpdate and a compare-and-swap on total_use.
  • Throws DiscountLimitReachedException::global and rolls back the transaction when the global limit was exhausted between cart validation and commit. No order is committed.
  • Throws DiscountLimitReachedException::perUser and rolls back when the discount is restricted to one use per customer and the customer has already redeemed it.
  • Snapshots discount_id, discount_code, discount_type, discount_value_at_apply and discount_currency_code onto the orders table for resilience against later discount edits or deletions.

DiscountValidator was updated to perform the same Order-based per-user check at cart-apply time so the rejection is surfaced before checkout.

Upgrade via:

composer require shopper/cart:^2.8 shopper/core:^2.8 php artisan migrate

Workarounds

None. Upgrade to v2.8.0.

Resources

Database specific
{
    "cwe_ids":  [
        "CWE-362"
    ],
    "github_reviewed":  true,
    "github_reviewed_at":  "2026-05-18T16:37:20Z",
    "nvd_published_at":  "2026-05-29T19:16:25Z",
    "severity":  "MODERATE"
}
References

Affected packages

Packagist / shopper/cart

Package

Name
shopper/cart
Purl
pkg:composer/shopper/cart

Affected ranges

Type
ECOSYSTEM
Events
Introduced
0 Unknown introduced version / All previous versions are affected
Fixed
2.8.0

Affected versions

v2.*
v2.6.0
v2.6.1
v2.6.2
v2.6.3
v2.6.4
v2.7.0
v2.7.1
v2.7.2
v2.7.3

Database specific

source
"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/05/GHSA-9rh9-hf3w-9fgg/GHSA-9rh9-hf3w-9fgg.json"