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:
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user