Harden booking backend against automated abuse - #50
Merged
Conversation
- require reCAPTCHA v3 on /api/booking/create (only unauthenticated endpoint) - expiring, revocable manage tokens: bind exp + token_version into the HMAC, rotate token_version on reschedule, verify against the stored version - rate-limit by trusted client IP only; drop the client-controlled User-Agent that let an attacker mint a fresh bucket per request - prevent double-booking via a partial unique index on (start_at) and by claiming the DB row before the calendar write (awaited rollback) - return a uniform 403 on lookup/cancel to avoid booking-id enumeration - add scripts/google-oauth-token.mjs to regenerate the Calendar refresh token Requires D1 migration migrations/0001 (token_version column + slot unique index).
- quote/escape ICS ATTENDEE params per RFC 5545 (mailer) - resolve dotlottie-web through dotlottie-react so pnpm's transitive layout finds the WASM asset
There was a problem hiding this comment.
Pull request overview
This PR hardens the booking backend against automated abuse and race conditions by adding reCAPTCHA v3 gating on booking creation, introducing expiring/versioned manage tokens, tightening rate-limit bucketing, and enforcing DB-level slot uniqueness with safer calendar/DB rollback behavior. It also includes ICS attendee escaping improvements, a Vite WASM asset resolution fix for pnpm, and adds a helper script for regenerating the Google OAuth refresh token.
Changes:
- Add reCAPTCHA v3 support for booking creation (client + server path) and reduce unnecessary reCAPTCHA loading on the frontend.
- Implement expiring, per-booking versioned HMAC manage tokens and update booking endpoints to verify against stored
token_version. - Prevent double-booking via a partial unique index and reorder create/reschedule to claim DB state before calendar writes, with awaited rollbacks.
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| vite.config.ts | Resolve dotlottie-web via dotlottie-react to ensure the WASM asset is found under pnpm’s layout. |
| src/services/api/booking.ts | Extend booking create payload to carry a reCAPTCHA token. |
| src/pages/BookingManage.tsx | Suppress referrer on the manage page to reduce manage-token leakage via outbound requests. |
| src/components/booking/BookingModal.tsx | Generate and submit a reCAPTCHA v3 token for booking creation. |
| src/components/booking/BookingButton.tsx | Mount reCAPTCHA provider only when the booking modal is open to avoid loading it globally. |
| scripts/google-oauth-token.mjs | Add a local helper to regenerate the Google Calendar OAuth refresh token. |
| migrations/README.md | Document how/when to apply D1 migrations required by the booking API. |
| migrations/0001_add_token_version_and_slot_uniqueness.sql | Add token_version and a partial unique index to enforce confirmed-slot uniqueness. |
| api/booking/reschedule.ts | Verify versioned tokens, claim slots in DB before calendar updates, rotate token version on success, and revert on failure. |
| api/booking/lookup.ts | Verify versioned tokens after fetching the row and return uniform 403 to reduce ID enumeration. |
| api/booking/create.ts | Add reCAPTCHA verification, claim slot in DB before calendar creation, and do awaited rollback on failures. |
| api/booking/cancel.ts | Verify versioned tokens after fetching the row and return uniform 403 to reduce ID enumeration. |
| api/booking/_lib/tokens.ts | Introduce expiring/versioned HMAC token format (exp.signature) and verification logic. |
| api/booking/_lib/mailer.ts | Tighten ICS escaping for attendee parameters per RFC 5545. |
| api/booking/_lib/d1.ts | Add a helper to detect unique-constraint violations for clean 409 handling. |
| api/_shared/rate-limit.ts | Stop using User-Agent in the bucket key; rate-limit primarily by client IP. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- remove axios (unused; sole path to the HIGH form-data CRLF advisory) - update dompurify 3.4.3 -> 3.4.11 (used in RichText) to clear DOMPurify advisories npm audit --omit=dev is now clean, unblocking the production audit gate.
pnpm 10.32.1 loads node:sqlite and requires Node >= 22.13, so the audit job crashed at setup on Node 20 (pre-existing). The deployed runtime stays Node 20 via Vercel/engines.
…ontrol chars - create/reschedule: check D1 meta.changes so a zero-row UPDATE (booking cancelled or vanished mid-flight) is an error/409 instead of a false success - quoteIcsParam: strip the full control-char range (C0 + DEL), not just DQUOTE/CR/LF - google-oauth-token: return a clear 400 when the callback lacks a code
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Security hardening of the booking backend, plus two small fixes. All content is already validated locally (109 tests pass, production build OK) and verified end-to-end against Google Calendar.
Security fixes
/api/booking/create— the only unauthenticated booking endpoint. The honeypot alone was trivially bypassable.token_versioninto the HMAC; rotatetoken_versionon reschedule; verify against the stored version. A leaked manage link is no longer a permanent bearer credential.User-Agentfrom the bucket key (rotating it minted a fresh bucket per request).(start_at) WHERE status='confirmed', and claim the DB row before the calendar write (awaited rollback, replacing a fire-and-forget one).Other fixes
ATTENDEEparam escaping (RFC 5545).dotlottie-webthroughdotlottie-reactfor pnpm's transitive layout (fixes the WASM asset in production builds).The matching D1 migration (
migrations/0001—token_versioncolumn + slot unique index) has already been applied to the production database. It is backward-compatible with the currently-deployed code, so ordering is safe. Merging this PR deploys the code that uses it.The Google Calendar OAuth refresh token has also been refreshed in the Vercel env (all environments); this deploy is needed for production to pick it up.