Race Condition Turns $10 Gift Card into $365
The Risk
A cinema chain's refund system could be tricked into crediting a gift card multiple times for a single ticket refund. A $5 ticket refund generated $90-100 in gift card credit each time, turning a $10 gift card into $365 over four tests. The gift card balance could be spent at any location in the chain, and anyone who downloaded the app had everything they needed to reproduce this.
The Vulnerability
The vulnerability chain started with the Android APK. Reverse engineering revealed all the credentials needed to interact with the booking API:
- An API token stored in plaintext in
res/raw/local_config.json - An HMAC signing key encrypted with AES-256 in
res/values/strings.xml, with the AES key hardcoded in the Java source
With these credentials, it was possible to authenticate as any loyalty member and interact with the booking API directly, bypassing the app entirely.
The Attack
The refund flow worked like this:
- App sends
POST /refundwith booking ID and gift card number - Application server checks: "Has this booking already been refunded?"
- If not, it forwards the credit to the downstream payment server
- Payment server credits the gift card and returns success
- Application server marks the booking as refunded
The problem: steps 2 and 5 are not atomic. When 20 requests arrive simultaneously, multiple pass the check at step 2 before any of them reach step 5. The payment server processes each credit independently.
Reproduction
The PoC script authenticated via the loyalty API using HMAC-signed headers, then fired 20 concurrent refund requests using a thread pool. Each request had independently generated HMAC signatures with unique request IDs.
The server responses broke down consistently across all four tests:
| Response | Count | Meaning |
|---|---|---|
| HTTP 200 + booking data | 1-2 | Refund officially processed |
| HTTP 500 "RefundAlreadyInProgress" | ~6 | Passed validation, credit likely committed |
| HTTP 400 "already refunded" | ~13 | Lock took effect, rejected |
The HTTP 500 responses are the key indicator. They prove the request passed application validation and reached the payment server before the lock. Each one results in a gift card credit.
The Impact
| Test | Before | After | Gain | Multiplier |
|---|---|---|---|---|
| 1 | $0.20 | $95.20 | +$95.00 | 19x |
| 2 | $88.60 | $178.60 | +$90.00 | 18x |
| 3 | $172.00 | $272.00 | +$100.00 | 20x |
| 4 | $265.40 | $365.40 | +$100.00 | 20x |
Each cycle cost ~$6.60 (one bargain ticket + booking fee) and returned $90-100 in gift card credit. The gift card balance could be used at any location in the cinema chain.
Why Mobile Matters Here
This vulnerability wouldn't have been found through web testing alone. The API credentials were embedded in the APK. The HMAC signing key was encrypted with AES, but the AES key was in the decompiled Java source. Without reversing the app, there's no way to authenticate with the booking API independently.
The race condition itself is a classic server-side flaw, but the attack surface was only accessible through the mobile app's API.
Remediation
- Implement atomic locking on refunds at the database level (e.g.,
SELECT FOR UPDATEor advisory locks) - Add server-generated idempotency keys that reject duplicate refund attempts
- Validate at the payment server level that total credits never exceed the original transaction amount
- Rotate the hardcoded API credentials and move to server-issued short-lived tokens
- Audit gift card balances for anomalous credit patterns
Responsible Testing
The gift card was purchased with personal funds ($10). The program was asked to disable and zero out the card after testing. No real customers were affected.