Skip to content

Harden booking backend against automated abuse - #50

Merged
rphlr merged 5 commits into
productionfrom
fix/booking-security-hardening
Jul 7, 2026
Merged

Harden booking backend against automated abuse#50
rphlr merged 5 commits into
productionfrom
fix/booking-security-hardening

Conversation

@rphlr

@rphlr rphlr commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

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

  • reCAPTCHA v3 on /api/booking/create — the only unauthenticated booking endpoint. The honeypot alone was trivially bypassable.
  • Expiring, revocable manage tokens — bind an expiry and a per-booking token_version into the HMAC; rotate token_version on reschedule; verify against the stored version. A leaked manage link is no longer a permanent bearer credential.
  • Rate-limit by trusted client IP only — drop the client-controlled User-Agent from the bucket key (rotating it minted a fresh bucket per request).
  • Prevent double-booking — partial unique index on (start_at) WHERE status='confirmed', and claim the DB row before the calendar write (awaited rollback, replacing a fire-and-forget one).
  • Uniform 403 on lookup/cancel — avoids booking-id enumeration.

Other fixes

  • Stricter ICS ATTENDEE param escaping (RFC 5545).
  • Resolve dotlottie-web through dotlottie-react for pnpm's transitive layout (fixes the WASM asset in production builds).

⚠️ Deploy note

The matching D1 migration (migrations/0001token_version column + 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.

rphlr added 2 commits July 7, 2026 12:10
- 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
Copilot AI review requested due to automatic review settings July 7, 2026 10:16

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread api/booking/create.ts
Comment thread api/booking/create.ts
Comment thread api/booking/reschedule.ts Outdated
Comment thread api/_shared/rate-limit.ts
Comment thread api/booking/_lib/mailer.ts
Comment thread scripts/google-oauth-token.mjs
rphlr added 3 commits July 7, 2026 12:41
- 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
@rphlr
rphlr merged commit b74edd3 into production Jul 7, 2026
4 checks passed
@rphlr
rphlr deleted the fix/booking-security-hardening branch July 7, 2026 12:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants