In flows where availability is checked when the user loads the booking form and the final insert happens minutes later at payment time, the check and the commit are separated by an unbounded time window. CWE-367 (TOCTOU race condition) names this exact pattern: the condition checked and the action taken are not atomic. A second user who books the same slot during the payment flow will not be detected until the UNIQUE constraint fires — and by then the first user has already been charged or committed their card details. Re-checking inside the transaction collapses the window to zero.
High because the TOCTOU window between initial check and final insert is exploitable under normal concurrent usage, not just adversarial load.
Move the availability query inside the same transaction block as the insert, in your booking creation handler (e.g., src/app/api/bookings/route.ts or src/lib/booking-service.ts).
await db.$transaction(async (tx) => {
// recheck — inside the transaction, atomic with the insert
const conflict = await tx.booking.findFirst({
where: { slotId, startTime, status: { not: 'CANCELLED' } }
});
if (conflict) throw new ConflictError('Slot taken');
await tx.booking.create({ data: { slotId, startTime, ...rest } });
});
ID: booking-flow-lifecycle.double-booking.slot-availability-recheck
Label: Availability re-checked before commit
Severity: high
What to look for: In flows where availability is checked early (e.g., at form load) then payment happens later, verify availability is re-checked inside the final transaction immediately before insertion. Before evaluating, extract and quote the transaction block that contains the final booking insert. Identify whether an availability query appears inside the same transaction scope (same $transaction callback, same BEGIN...COMMIT block).
Pass criteria: Count all booking creation transaction blocks. At least 1 must contain an availability recheck query inside the same transaction scope. The check and insert happen atomically — both must appear within the same transaction callback or SQL transaction block. The recheck must query for conflicting bookings (not just check a boolean flag).
Fail criteria: Availability is checked at the start of the request, but not verified again during the final database write. 0% of transaction blocks contain an availability recheck. A race condition window exists between check and insert. Also fails if the recheck is in a separate transaction from the insert.
Skip (N/A) when: The entire operation is a single atomic SQL statement (e.g., INSERT ... WHERE NOT EXISTS ...) or the UNIQUE constraint guarantees atomicity without an explicit recheck.
Detail on fail: "Availability is verified when the user loads the payment form, but not re-verified before the final booking insert. Another user could book the slot in between."
Cross-reference: The conflict-check in Booking Creation verifies the initial availability check that this recheck reinforces.
Cross-reference: The concurrent-request-handling check in this category covers the database constraint that provides ultimate protection.
Cross-reference: The rescheduling-logic check in Lifecycle Management verifies atomic rechecks during reschedule operations.
Remediation: Move the availability check into the transaction block in your booking creation handler.
await db.$transaction(async (tx) => {
// Re-check availability inside transaction
const conflict = await tx.booking.findFirst({
where: { slotId, status: { not: 'CANCELLED' } }
});
if (conflict) throw new Error("Slot taken");
// Only then insert
await tx.booking.create({ data: {...} });
});