Skip to content

feat: add CSRF protection to /api/book/event and affiliated booking endpoints#27936

Draft
emrysal wants to merge 2 commits intomainfrom
devin/1770987825-csrf-booking-endpoints
Draft

feat: add CSRF protection to /api/book/event and affiliated booking endpoints#27936
emrysal wants to merge 2 commits intomainfrom
devin/1770987825-csrf-booking-endpoints

Conversation

@emrysal
Copy link
Contributor

@emrysal emrysal commented Feb 13, 2026

What does this PR do?

Adds CSRF protection to the three booking creation endpoints that previously lacked it:

  • POST /api/book/event
  • POST /api/book/recurring-event
  • POST /api/book/instant-event

This follows the same double-submit cookie pattern already used by /api/cancel and /api/video/guest-session: client fetches a token from GET /api/csrf (which also sets an httpOnly cookie), includes the token in the request body, and the server validates that they match.

Server-side

  • Adds validateCsrfTokenForPagesRouter() to apps/web/lib/validateCsrfToken.ts — a Pages Router variant of the existing App Router validateCsrfToken(), since these endpoints use Pages Router API routes (req.cookies instead of cookies() from next/headers).
  • Each of the three booking handlers now calls this validation as its first step, throwing a 403 HttpError on mismatch.
  • Handler signatures updated to accept res: NextApiResponse (needed to clear the CSRF cookie after validation).

Client-side

  • Adds packages/features/bookings/lib/fetchCsrfToken.ts — a small helper that calls GET /api/csrf.
  • create-booking.ts, create-recurring-booking.ts, and create-instant-booking.ts each fetch a CSRF token before POSTing, and include csrfToken in the request body.
  • For recurring events (where the body is an array), the token is attached to every element; the server reads it from body[0].

Tests

  • 9 unit tests for validateCsrfTokenForPagesRouter (happy path, missing/wrong/mismatched token, array body, cookie clearing).
  • 2 unit tests for fetchCsrfToken.
  • Updated existing recurring-event.test.ts to pass res to the handler and mock CSRF validation (these tests cover booking logic, not CSRF — CSRF has its own dedicated test suite).

⚠️ Important items for reviewer

  1. Embed / cross-origin scenarios: The CSRF cookie (calcom.csrf_token) is set with SameSite=Lax by default via GET /api/csrf. Embeds that render the Booker in an iframe on a third-party domain may need SameSite=None (the /api/csrf endpoint already supports a ?sameSite=none query param). Please verify that embedded booking flows still work — the Booker client code calls the same createBooking functions being modified here.

  2. Cookie header handling: The Pages Router version clears the cookie via res.setHeader("Set-Cookie", ...). If other middleware also sets Set-Cookie headers, this could overwrite them. Cubic AI flagged this (confidence 8/10). Worth confirming this is safe in the current middleware stack — if not, switching to res.appendHeader would be the fix.

  3. Handler signature change: The three booking handlers now accept (req, res) instead of just (req). Any code that calls the named exports (e.g. handleRecurringEventBooking) directly — rather than through the defaultResponder default export — will need to pass res. The existing recurring-event.test.ts was the only caller found and has been updated.

Mandatory Tasks (DO NOT REMOVE)

  • I have self-reviewed the code (A decent size PR without self-review might be rejected).
  • I have updated the developer docs in /docs if this PR makes changes that would require a documentation change. N/A — no public API contract changes, internal security hardening only.
  • I confirm automated tests are in place that prove my fix is effective or that my feature works.

How should this be tested?

  1. Unit tests: TZ=UTC yarn vitest run apps/web/lib/validateCsrfToken.test.ts packages/features/bookings/lib/fetchCsrfToken.test.ts — all 11 tests should pass.

  2. Manual testing (happy path):

    • Start the dev server (yarn dx)
    • Navigate to a public booking page (e.g., /pro/30min)
    • Complete a booking — should succeed as normal
    • Check Network tab: you should see a GET /api/csrf request before the POST /api/book/event request
  3. Manual testing (CSRF rejection):

    • Use browser devtools to delete the calcom.csrf_token cookie after the /api/csrf call but before submitting the booking form
    • Submit the booking — should receive a 403 "Invalid CSRF token" error
  4. Embed testing (if applicable):

    • Test the embed snippet on a third-party domain to ensure bookings still work

Checklist

  • My code follows the style guidelines of this project
  • I have checked if my changes generate no new warnings
  • My PR is not too large (<500 lines, <10 files)

Link to Devin run: https://app.devin.ai/sessions/85e471b18922411e9b66b2450d81441d
Requested by: @emrysal

…stant-event endpoints

Co-Authored-By: alex@cal.com <me@alexvanandel.com>
@devin-ai-integration
Copy link
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR that start with 'DevinAI' or '@devin'.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

2 issues found across 10 files

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="apps/web/lib/validateCsrfToken.ts">

<violation number="1" location="apps/web/lib/validateCsrfToken.ts:34">
P2: The cookie-clearing header is missing the `Secure` flag that the `/api/csrf` route uses when setting the cookie in HTTPS environments. For consistency and robustness, derive the `Secure` attribute from the request protocol (or `WEBAPP_URL`) rather than hardcoding.</violation>

<violation number="2" location="apps/web/lib/validateCsrfToken.ts:34">
P1: `res.setHeader("Set-Cookie", ...)` overwrites all previously set `Set-Cookie` headers on the response, including those added by middleware (e.g., session cookies). Use `res.appendHeader` instead to safely add the clearing cookie without discarding existing ones.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

throw new HttpError({ statusCode: 403, message: "Invalid CSRF token" });
}

res.setHeader("Set-Cookie", `${CSRF_COOKIE_NAME}=; Path=/; Max-Age=0; HttpOnly; SameSite=Lax`);
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 13, 2026

Choose a reason for hiding this comment

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

P1: res.setHeader("Set-Cookie", ...) overwrites all previously set Set-Cookie headers on the response, including those added by middleware (e.g., session cookies). Use res.appendHeader instead to safely add the clearing cookie without discarding existing ones.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/lib/validateCsrfToken.ts, line 34:

<comment>`res.setHeader("Set-Cookie", ...)` overwrites all previously set `Set-Cookie` headers on the response, including those added by middleware (e.g., session cookies). Use `res.appendHeader` instead to safely add the clearing cookie without discarding existing ones.</comment>

<file context>
@@ -1,13 +1,35 @@
+    throw new HttpError({ statusCode: 403, message: "Invalid CSRF token" });
+  }
+
+  res.setHeader("Set-Cookie", `${CSRF_COOKIE_NAME}=; Path=/; Max-Age=0; HttpOnly; SameSite=Lax`);
+}
</file context>
Fix with Cubic

@github-actions
Copy link
Contributor

github-actions bot commented Feb 13, 2026

Devin AI is addressing Cubic AI's review feedback

New feedback has been sent to the existing Devin session.

View Devin Session


✅ Pushed commit 7d4db9e

Cubic AI feedback review:

  • Violation 1 (P1, res.setHeader overwrites cookies): confidence 8/10 — below 9/10 threshold, skipped
  • Violation 2 (P2, missing Secure flag): confidence 7/10 — below 9/10 threshold, skipped

CI fix: Updated recurring-event.test.ts to pass res to the handler and mock CSRF validation (these tests cover booking logic, not CSRF).

…ation

Co-Authored-By: alex@cal.com <me@alexvanandel.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant