Skip to content

🐛 Promo Code Max-Usage Limit Bypass in Hi.Events (Async Counter Race Condition) #1223

Description

@geo-chen

(reported via email on 24 May - no response)

I am reporting a vulnerability in Hi.Events (develop branch, v1.8.0-beta) that allows any user to bypass promo code max-usage limits, enabling unlimited redemption of codes intended to be single-use or otherwise restricted.

Affected component: order reservation and promo code validation flow
CWE: CWE-362 (Race Condition / Improper Synchronization)
CVSS 3.1 Score: 7.5 (High) -- AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N

Summary:

When a buyer reserves an order, Hi.Events validates the promo code by reading the current order_usage_count from the database and comparing it to max_allowed_usages. However, the usage count is only incremented asynchronously by a queue job (UpdateEventStatisticsJob) that fires after an order is completed. The order completion handler (CompleteOrderHandler) does not re-validate the promo code.

This means an attacker can reserve multiple orders with the same limited promo code in sequence. Each reservation reads order_usage_count=0 (because no order has been completed yet), passes validation, and locks in the discount. All reserved orders can then be completed, each receiving the discounted price. No concurrent requests are required.

Reproduction steps (tested against local Docker, port 9113):

  1. Create a promo code with max_allowed_usages=1 and a percentage discount (e.g., 50% off).
  2. Submit POST /api/events/{id}/orders with the promo code applied. Note the discounted total in the response.
  3. Without completing the first order, submit a second POST /api/events/{id}/orders with the same promo code. The discount is applied again.
  4. Repeat for any number of additional reservations. Each one succeeds.
  5. Complete each reserved order. All completions succeed; CompleteOrderHandler does not re-check the promo code.

Result: all orders receive the discount regardless of the max_allowed_usages setting.

Root cause:

  • CreateOrderHandler reads promo code validity inside a per-event advisory lock, but the lock only serializes concurrent reservations for the same event. It does not prevent sequential reservations from reading a stale count.
  • CompleteOrderHandler does not re-validate promo code eligibility before finalizing an order.
  • The usage counter is incremented asynchronously (UpdateEventStatsListener dispatches UpdateEventStatisticsJob only when an order reaches completed status), so the count is 0 throughout the reservation phase.

Recommended fix:

Re-validate the promo code (including usage count) inside CompleteOrderHandler before marking an order as completed. Alternatively, increment the promo code usage count synchronously at reservation time (within the advisory lock transaction) and decrement it if the order expires or is cancelled.

Impact:

Event organizers suffer direct financial loss. Any user with access to a limited promo code can apply it an unlimited number of times. Single-use codes (intended for one person) can be reused for all attendees at an event.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions