Security release: the second batch from the researcher behind 4.1.1, confirmed against 4.1.1 and
fixed here. Both only matter with slotHolds.enabled (the first) or a database having a bad day
(the second). No schema change, no migration. Two new options on slotHolds, both with safe
defaults.
Security
/reserve/holdwas unauthenticated and uncapped. A hold costs nothing to take and needed no
login, so one anonymous script could hold every future slot of a resource and re-hold each one
as it lapsed, keeping the schedule unbookable for as long as it ran. No data exposed, and it
self-heals when the script stops, but a real availability hole. Two new knobs:
slotHolds.maxActivePerCustomer(default5,falseto disable) caps the unexpired holds one
signed-in customer may have (429 { error: 'hold_limit_reached', detail }past it; staff exempt),
andslotHolds.requireAuth(defaultfalse) refuses anonymous callers with
401 { error: 'authentication_required' }. It stays off by default because holds exist so a
customer can claim a slot before an account exists. If you keep anonymous holds, rate-limit
POST /api/reserve/holdat your proxy or middleware — a Payload handler has no trustworthy
client address to throttle on, so the plugin does not pretend to.- The conflict check fell back to zero buffers when a service lookup failed.
bufferFor
swallowed any error from reading a neighbouring reservation's service, logged it only under
debug, and carried on with0/0for the rest of the call — so a transient database error let a
back-to-back booking through the gap the buffer should have blocked, silently. It now fails
closed: the availability check rejects (a booking or hold gets a 500, never a slip-through) and
the failure is logged aterrorlevel regardless ofdebug. A service that no longer exists (a
dangling reference, only reachable past the delete guard withcontext.skipReservationHooks)
still resolves to zero buffer, now with awarnline.
Added
slotHolds.requireAuthandslotHolds.maxActivePerCustomer(validated at init: a positive
integer orfalse).HoldRefusalReasongainsauthentication_required(401) andhold_limit_reached(429).