chore: CSRF protect cancel functionality (#23439)

* add csrf endpoint

* Update booking-pages.e2e.ts

* Update zod-utils.ts

* fix type error
This commit is contained in:
Anik Dhabal Babu
2025-09-01 23:18:50 +00:00
committed by GitHub
parent 2e9aa01771
commit f6f36750ec
5 changed files with 40 additions and 2 deletions
+10 -2
View File
@@ -5,7 +5,7 @@ import type { NextRequest } from "next/server";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import handleCancelBooking from "@calcom/features/bookings/lib/handleCancelBooking";
import { bookingCancelInput } from "@calcom/prisma/zod-utils";
import { bookingCancelWithCsrfSchema } from "@calcom/prisma/zod-utils";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
@@ -16,8 +16,16 @@ async function handler(req: NextRequest) {
} catch (error) {
return NextResponse.json({ success: false, message: "Invalid JSON" }, { status: 400 });
}
const bookingData = bookingCancelWithCsrfSchema.parse(appDirRequestBody);
const cookieStore = await cookies();
const cookieToken = cookieStore.get("calcom.csrf_token")?.value;
if (!cookieToken || cookieToken !== bookingData.csrfToken) {
return NextResponse.json({ success: false, message: "Invalid CSRF token" }, { status: 403 });
}
cookieStore.delete("calcom.csrf_token");
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
const bookingData = bookingCancelInput.parse(appDirRequestBody);
const result = await handleCancelBooking({
bookingData,
userId: session?.user?.id || -1,
+17
View File
@@ -0,0 +1,17 @@
import { randomBytes } from "crypto";
import { NextResponse } from "next/server";
export async function GET() {
const token = randomBytes(32).toString("hex");
const res = NextResponse.json({ csrfToken: token });
res.cookies.set("calcom.csrf_token", token, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
path: "/",
});
return res;
}
@@ -206,6 +206,9 @@ export default function CancelBooking(props: Props) {
telemetry.event(telemetryEventTypes.bookingCancelled, collectPageParameters());
const response = await fetch("/api/csrf", { cache: "no-store" });
const { csrfToken } = await response.json();
const res = await fetch("/api/cancel", {
body: JSON.stringify({
uid: booking?.uid,
@@ -215,6 +218,7 @@ export default function CancelBooking(props: Props) {
seatReferenceUid,
cancelledBy: currentUserEmail,
internalNote: internalNote,
csrfToken,
}),
headers: {
"Content-Type": "application/json",
+3
View File
@@ -656,9 +656,12 @@ test.describe("Event type with disabled cancellation and rescheduling", () => {
});
test("Should prevent cancellation and show an error message", async ({ page }) => {
const csrfTokenResponse = await page.request.get("/api/csrf");
const { csrfToken } = await csrfTokenResponse.json();
const response = await page.request.post("/api/cancel", {
data: {
uid: bookingId,
csrfToken,
},
headers: {
"Content-Type": "application/json",
+6
View File
@@ -309,6 +309,12 @@ export const bookingCancelInput = bookingCancelSchema.refine(
"At least one of the following required: 'id', 'uid'."
);
export const bookingCancelWithCsrfSchema = bookingCancelSchema
.extend({
csrfToken: z.string().length(64, "Invalid CSRF token"),
})
.refine((data) => !!data.id || !!data.uid, "At least one of the following required: 'id', 'uid'.");
export const vitalSettingsUpdateSchema = z.object({
connected: z.boolean().optional(),
selectedParam: z.string().optional(),