Safeguard
Application Security•

ASVS 5.0 V2.3.4: Lock Limited Resources to Stop Double Booking

ASVS 5.0 V2.3.4 (Level 2) requires locking so seats and slots cannot be double booked. SQL locking and constraint patterns, with a concurrent test.

Safeguard Security Team
4 min read

ASVS 5.0 V2.3.4 requires business logic level locking so that limited quantity resources, such as theater seats or delivery slots, cannot be double booked by manipulating the application's logic. It is a Level 2 requirement.

Why it matters

Two people click Buy on the last seat at the same moment. If the code reads availability, then writes, both reads see one seat free and both writes succeed. Attackers use this deliberately: they send dozens of parallel requests to claim stock, win limited drops, reuse a single use voucher, or spend one balance twice. This is called a race condition, and it is easy to miss because everything works in single user testing.

How to implement it

Let the database enforce exclusivity. The strongest option is a unique constraint, because no code path can bypass it.

CREATE TABLE seat_booking (
  event_id  bigint NOT NULL,
  seat_no   int    NOT NULL,
  user_id   bigint NOT NULL,
  PRIMARY KEY (event_id, seat_no)
);

A second insert for the same seat fails with a constraint error, which you turn into a friendly "seat taken" response.

For counted stock, use an atomic conditional update:

UPDATE inventory SET quantity = quantity - 1
WHERE sku = :sku AND quantity > 0;

Check the number of rows changed. Zero means sold out. When you must read and then decide, take a row lock with SELECT ... FOR UPDATE inside a transaction. For temporary holds, store an expiry time and release expired holds with a scheduled job, so abandoned carts do not block inventory forever.

Do not use an application level lock held in one process memory when you run several instances. If you need a distributed lock, use one with expiry and a fencing token, and still keep the database constraint as the final guard.

How to test it

Write a test that fires 50 parallel requests for a single seat, each from a different test account. Exactly one should succeed. Repeat for voucher redemption and balance spend. Run it repeatedly, since races appear intermittently. Also test the release path: cancel a booking and confirm the seat becomes available once.

Holds, expiry and fairness

Real systems rarely book instantly. The user picks a seat, then pays. Model this with a hold that has an owner and an expiry time. The row for a held seat carries the user id and a held_until timestamp. Booking succeeds only for that user before expiry, and a cleanup job or a conditional query frees expired holds.

UPDATE seat SET held_by = :user, held_until = now() + interval '10 minutes'
WHERE event_id = :event AND seat_no = :seat
  AND (held_by IS NULL OR held_until < now());

Zero changed rows means someone else has it. Cap how many holds one account can have at once, or one script will reserve everything and release it later. That limit is the anti automation control from V2.4.1 at work.

Idempotent payments

Payment retries must not create two bookings. Use an idempotency key, and make the booking row the single source of truth. If a payment webhook arrives twice, the second call finds the booking already confirmed and does nothing.

What to log

Log hold, confirm, release and expiry events with user and resource ids. When someone reports a double booking, those records let you tell a code bug from deliberate abuse.

Common mistakes

  • Read, check, then write as separate statements outside a transaction.
  • Relying on a lock held only in application memory.
  • Locks without a timeout, which freeze the resource when a request dies.
  • Testing only with sequential requests.

Quick check

Why is a unique constraint better than a check in code?

The database applies it atomically for every writer, including code paths and scripts that forget the check.

How do you check that stock never goes negative?

Use a conditional update with a quantity greater than zero condition, and test with parallel purchases.

Sources

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.