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.
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.
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
}
ID: booking-calendar-availability.calendar-display.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 matching src/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
}