# Sylius has a Promotion Usage Limit Bypass via Race Condition

TOCTOU race in Sylius promotion/coupon usage limits lets concurrent order completions redeem single-use promotions unlimited times, causing financial loss.

- Payout: Research
- Severity: high
- Program: Sylius
- Found by: whiteov3rflow, bnBart
- Disclosed: 2026-03-11
- CVE: CVE-2026-31824
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-7mp4-25j8-hp5q
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-7mp4-25j8-hp5q
- Tags: area:web3 domain:web impact:funds-loss kind:research platform:ghsa sev:high tech:php technique:race-window vuln:accounting-error vuln:business-logic vuln:race-condition

**Target:** Sylius shop order completion API (PATCH /api/v2/shop/orders/{token}/complete) and promotion/coupon usage counters
**Root cause:** Eligibility checks read the in-memory Doctrine `used` counter during validation, while the increment in OrderPromotionsUsageModifier happens later at order completion with no DB locking or atomic increment; Doctrine flushes absolute values (`SET used = 1`) and entities lack optimistic locking.
**Steps:**
1. Prepare multiple carts each containing the same limited-use promotion or coupon.
2. Fire simultaneous `PATCH /api/v2/shop/orders/{token}/complete` requests for those carts.
3. All requests read the same stale `used` count, pass eligibility checks, and complete successfully.
4. Repeat to redeem a single-use promotion/coupon an arbitrary number of times; per-customer limit bypassed by one customer completing multiple orders concurrently.
**How found:** Noticed the eligibility check reads the in-memory Doctrine `used` counter while the increment occurs later at order completion, with no locking; Doctrine flushes absolute values rather than atomic increments and entities lack optimistic locking, so concurrent requests share stale counts.
**Exploitation:** Unauthenticated attacker prepares many carts with the same limited-use coupon/promotion and races concurrent order-completion requests so all pass the usage-limit check before any increment is persisted, yielding unlimited redemptions and direct discount/financial loss.
**Insight:** In Doctrine/Symfony shops, usage-limit checks that read entity state during validation but increment at a later lifecycle phase are raceable; test by racing the final state-transition endpoint (order complete) rather than the add-to-cart step, and check whether counters use absolute flush vs atomic `used = used + 1`.

