Buffer time between appointments enforced at the data layer
Why it matters
Buffer time enforced only in the UI is not enforced. Any client that posts directly to the booking API — a script, a browser extension, a race condition — bypasses the disabled slots entirely and books into the buffer window. This violates CWE-362 (race condition) and CWE-667 (improper locking), and the ISO 25010 functional-correctness attribute. The practical result: a host has back-to-back appointments with no travel or prep time, they run late, the second client has a bad experience, and a refund is likely. Server-side enforcement is the only enforcement that matters.
Severity rationale
Critical because UI-only buffer checks are trivially bypassed with a direct API call, making double-booking into the buffer window mechanically possible without any exploit skills.
Remediation
Re-validate buffer time inside the booking API route, not just when filtering available slots for display. Define BUFFER_MINUTES as a server-side constant and query for conflicts that include the buffer window before confirming.
// src/app/api/book/route.ts
const BUFFER_MINUTES = 15
export async function POST(req: Request) {
const { startTime, duration, resourceId } = await req.json()
const start = new Date(startTime)
const end = new Date(start.getTime() + duration * 60_000)
const conflict = await db.bookings.findFirst({
where: {
resourceId,
status: 'confirmed',
startTime: { lt: new Date(end.getTime() + BUFFER_MINUTES * 60_000) },
endTime: { gt: new Date(start.getTime() - BUFFER_MINUTES * 60_000) },
},
})
if (conflict) {
return Response.json({ error: 'Buffer conflict' }, { status: 409 })
}
// ... create booking
}
Detection
-
ID:
buffer-enforcement -
Severity:
critical -
What to look for: Before evaluating, extract and quote the exact buffer time constant or configuration value (e.g.,
BUFFER_TIME = 15,bufferMinutes: 15) from the codebase. Find the logic that computes or filters available slots. Count all booking API endpoints (e.g.,POST /api/book,POST /api/appointments). For each, verify that buffer time is validated server-side — in a database constraint, trigger, API validation function, or transaction. UI-only enforcement is not sufficient — a direct API call bypasses it. Examine files matchingsrc/app/api/book*,src/app/api/appointment*,src/lib/*availability*,prisma/schema.prisma,drizzle/*. -
Pass criteria: Count all booking creation API endpoints. 100% of booking endpoints must validate buffer time server-side before confirming a booking. A database constraint, API middleware, or transaction must prevent booking a slot if it would violate the buffer time requirement. Example: if buffer is 15 minutes and a meeting ends at 2:00 PM, the next available slot cannot start before 2:15 PM. Report: "X of Y booking endpoints enforce buffer time at the data layer."
-
Fail criteria: Buffer time is only checked in the UI (e.g., a disabled slot), or a direct API call can bypass the buffer check.
-
Do NOT pass when: Buffer time logic exists only in a client-side component (e.g.,
CalendarSlot.tsx) but not in any API route or database constraint. Client-side-only validation is insufficient regardless of how thorough it appears. -
Skip (N/A) when: Never — buffer time enforcement is essential to prevent scheduling conflicts.
-
Detail on fail: Example:
"UI shows blocked slots 15 minutes after each appointment, but the booking API does not validate buffer time. A direct POST to /api/book bypasses the restriction." -
Cross-reference: Check
data-freshness— stale availability data means the client does not know about recent bookings, making buffer violations more likely. -
Cross-reference: Check
duration-validation— buffer time and duration validation often share the same API middleware. -
Cross-reference: Check
same-day-cutoff— all booking time validations should occur at the same API layer for consistency. -
Remediation: Enforce buffer time at the API layer:
// pages/api/availability.ts const BUFFER_MINUTES = 15 export async function getAvailableSlots( date: Date, duration: number, resourceId: string ) { const bookings = await db.bookings.findMany({ where: { resourceId, startTime: { gte: new Date(date.getTime() - 24 * 60 * 60 * 1000) }, status: 'confirmed', }, }) const slots = generateSlots(date, 30) // 30-minute increments return slots.filter((slot) => { const slotEnd = new Date(slot.time.getTime() + duration * 60 * 1000) const bufferStart = new Date(slot.time.getTime() - BUFFER_MINUTES * 60 * 1000) const bufferEnd = new Date(slotEnd.getTime() + BUFFER_MINUTES * 60 * 1000) return !bookings.some( (booking) => booking.startTime < bufferEnd && booking.endTime > bufferStart ) }) } export async function POST(req: Request) { const { slotTime, duration, resourceId } = await req.json() // Re-validate buffer time at booking time const conflicts = await db.bookings.findMany({ where: { resourceId, startTime: { lt: new Date(slotTime.getTime() + (duration + BUFFER_MINUTES) * 60 * 1000), }, endTime: { gt: new Date(slotTime.getTime() - BUFFER_MINUTES * 60 * 1000), }, status: 'confirmed', }, }) if (conflicts.length > 0) { return Response.json( { error: 'Slot no longer available due to buffer time' }, { status: 409 } ) } // ... create booking }
External references
- cwe · CWE-362 — Race Condition — concurrent booking requests can both pass a client-only buffer check
- cwe · CWE-667 — Improper Locking — buffer enforcement requires server-side transactional lock
- iso-25010:2011 · functional-correctness — Functional Correctness — scheduling constraints must be enforced end-to-end
Taxons
History
- 2026-04-18·v1.0.0·Initial import from booking-calendar-availability·automated