TOCTOU race allows double-booking a slot (non-transactional) #33
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Severity: Medium — integrity / potential double-charge.
Problem
BookingEndpoint::book()(src/Booking/BookingEndpoint.php:107-155) checks$slot->isBooked, then later callsinsert()+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
insertSeriespath 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.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.