Application Security

Concurrent Requests Break Business Rules That Look Airtight

Key takeaway: A rule enforced by reading state and then writing it is not enforced at all under concurrency. The gap between the two operations is where the attacker operates.

The Pattern That Fails

Code reads a balance, confirms it is sufficient, then deducts. Or reads a coupon’s redemption count, confirms it is unused, then marks it used. Or counts existing invitations, confirms the limit is not reached, then inserts.

Every one of these is correct when requests arrive one at a time. Send fifty simultaneously and all fifty read the original state, all fifty pass the check, and all fifty proceed. The limit was enforced against a value that was already stale when it was read.

The exploitation requires no sophistication — a script sending parallel requests, or a browser tool replaying one request many times at once.

Where It Shows Up Financially

The consequences cluster in the places with direct monetary value.

  • Redeeming a single-use voucher many times
  • Withdrawing more than an account holds
  • Applying a referral bonus repeatedly
  • Exceeding a rate limit or quota
  • Claiming a limited-quantity item more times than stock allows
  • Escalating a two-step approval by racing both steps

Loyalty and rewards systems are hit disproportionately because they combine real value with logic that was rarely designed for concurrency and is often implemented outside the core transactional system.

Fixes That Actually Hold

Technique Effect
Application-level check then write No protection
Database unique constraint Strong for uniqueness rules
SELECT ... FOR UPDATE Serialises access to the row
Atomic conditional update Strong, single statement
Optimistic locking with version column Strong, needs retry handling
Serializable isolation Strong, costs throughput

The atomic conditional update is the cleanest option where it applies. A single statement that decrements a balance only if the balance is sufficient, checking rows affected, cannot be raced because the condition and the write are one operation the database evaluates under its own locking.

Unique constraints are the most underused. A rule like “one redemption per user per voucher” is a uniqueness property, and the database enforces uniqueness correctly under any concurrency. Enforcing it in application code instead is choosing a weaker mechanism for something the database already does perfectly.

Row locking with FOR UPDATE works and requires care about lock ordering to avoid deadlock, plus discipline about transaction duration.

Testing For It

Sequential tests never find these bugs, which is why they reach production despite good coverage.

Write tests that fire the same operation concurrently — twenty parallel calls redeeming one voucher — and assert that exactly one succeeds. These tests are straightforward to write and reliably catch the entire class.

Include the cross-endpoint cases too. Some races involve two different operations on the same state, and testing each endpoint alone will not reveal them.

The Bottom Line

Move every invariant into the database as a constraint or an atomic conditional update rather than a read followed by a write. Add concurrent tests asserting that exactly one of N simultaneous operations succeeds, since that is the only kind of test that finds this class of bug.

Related Articles

Leave a Reply

Your email address will not be published. Required fields are marked *

Back to top button