GraphQL Gift Card PIN Enumeration with No Rate Limiting
The Risk
Anyone with a free account on a major fashion e-commerce platform and one purchased gift card could find and instantly spend other customers' unredeemed gift cards. There was no lockout, no slowdown, and no warning to the legitimate recipient. During testing, a $100 card belonging to a real customer was found and redeemed in under twenty seconds. Cards on the platform sell for up to $500 each and are issued in bulk for corporate gifting, so the value at risk was significant.
The Vulnerability
Two flaws compounded into instant gift card theft:
1. No rate limiting on the GraphQL redemption mutation
The customerRedeemGiftCard mutation on the GraphQL endpoint had no rate limit. The equivalent legacy REST endpoint was protected by a Cloudflare rule that triggered after roughly 22 requests, but that rule did not cover the GraphQL endpoint. During testing, 23,000 redemption probes were sent in 85 seconds (about 270 requests per second) with zero throttling, no challenge, and no lockout.
2. Sequential PIN issuance within a batch
Gift card PINs were 19 digits long with predictable structure:
6087480024515610906
^^^^^^^^ fixed prefix
^^^^^ batch ID (sequential per issuance window)
^^^ fixed segment
^^^ counter (sequential within batch) Cards in the same batch shared the same batch ID. Within a batch the counter incremented predictably as cards were issued. An attacker holding any one card knew that cards within plus or minus a hundred positions had been issued in the same window.
The Attack
- The attacker creates a free account on the platform and obtains a bearer token by logging in.
- They purchase one low-value gift card to use as a seed position in the issuance sequence.
- They run an asynchronous script that fires the redemption mutation against PIN values plus or minus 200 from the seed, using 20 concurrent workers.
- Invalid PINs return
BAD_REQUESTwith HTTP 200. Valid unredeemed PINs return aStoreCreditobject with the card's value and a transaction ID, and the card is instantly credited to the attacker's account. - The full 203-probe scan completes in under 20 seconds. With 50 concurrent workers it finishes in under 10.
Confirmed test result
One scan was run from the seed position. A $100 gift card belonging to a real third-party customer was found at offset +91 and redeemed in a single mutation. The card needed to be reinstated for the original recipient as part of the disclosure.
| Field | Value |
|---|---|
| Card found at offset | +91 from seed |
| Value stolen | $100.00 |
| Probes sent | 203 |
| Time taken | Approximately 18.5 seconds |
| Concurrency | 20 workers |
The Impact
Gift cards on the platform were sold in denominations up to $500 and issued in bulk for corporate gifting and promotions. An attacker who purchased one low-value card obtained a position in the issuance sequence and could scan adjacent positions at unlimited speed. The attack scaled linearly with concurrency.
There was no detection on the server side, no lockout on repeated bad PINs, and no notification to the legitimate recipient. A stolen card could not be refunded after the fact because the redemption was atomic. The legitimate recipient would only discover the theft when they tried to use a card that already showed a zero balance.
Remediation
- Apply rate limiting to the redemption mutation. Three to five attempts per account per hour matches the existing protection on the REST endpoint.
- Extend the existing WAF rate-limit rule to cover the GraphQL endpoint, not just the legacy REST path.
- Replace sequential or near-sequential PIN generation with cryptographically random values of at least 128 bits of entropy. Predictable structure inside a PIN must be removed.
- Add a CAPTCHA or step-up challenge after two consecutive failed redemption attempts in a single session.
- Alert on sustained redemption failure patterns from a single account and lock the account pending review.