TOCTOU race allows double-booking a slot (non-transactional) #33

Closed
opened 2026-06-09 19:08:19 +00:00 by thatguygriff · 1 comment
Owner

Severity: Medium — integrity / potential double-charge.

Problem

BookingEndpoint::book() (src/Booking/BookingEndpoint.php:107-155) checks $slot->isBooked, then later calls insert() + markBooked() with no row lock or transaction. Two concurrent requests for the same slot both pass the check and both book it (and both may be charged).

The insertSeries path is also non-transactional — a partial failure leaves orphaned lesson rows and half-marked slots.

Fix

Use a conditional atomic update (UPDATE ... SET is_booked = 1 WHERE id = ? AND is_booked = 0) and act on the affected-row count to claim the slot before inserting the lesson, or wrap the claim+insert in a DB transaction. Apply the same for the series path.

**Severity: Medium** — integrity / potential double-charge. ## Problem `BookingEndpoint::book()` ([src/Booking/BookingEndpoint.php:107-155](src/Booking/BookingEndpoint.php#L107-L155)) checks `$slot->isBooked`, then later calls `insert()` + `markBooked()` with no row lock or transaction. Two concurrent requests for the same slot both pass the check and both book it (and both may be charged). The `insertSeries` path is also non-transactional — a partial failure leaves orphaned lesson rows and half-marked slots. ## Fix Use a conditional atomic update (`UPDATE ... SET is_booked = 1 WHERE id = ? AND is_booked = 0`) and act on the affected-row count to claim the slot before inserting the lesson, or wrap the claim+insert in a DB transaction. Apply the same for the series path.
thatguygriff added the bugsecurity labels 2026-06-09 19:08:19 +00:00
Author
Owner

Verified resolved on main (061d09e, PR #38): slots are claimed via an atomic guarded update — AvailabilityRepository::claim() runs UPDATE ... SET is_booked = 1 WHERE id = %d AND is_booked = 0 and the lesson is only inserted when the affected-row count confirms this request won the claim. The weekly path claims each occurrence individually and creates lessons only for the slots actually claimed, so a racing request never double-books and no half-marked rows are left behind. Re-confirmed during the 2026-06-10 security review pass.

Verified resolved on main (061d09e, PR #38): slots are claimed via an atomic guarded update — AvailabilityRepository::claim() runs UPDATE ... SET is_booked = 1 WHERE id = %d AND is_booked = 0 and the lesson is only inserted when the affected-row count confirms this request won the claim. The weekly path claims each occurrence individually and creates lessons only for the slots actually claimed, so a racing request never double-books and no half-marked rows are left behind. Re-confirmed during the 2026-06-10 security review pass.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Unsupervised/unsupervised-scheduler#33