diff --git a/apps/web/lib/csp.ts b/apps/web/lib/csp.ts index 257f0d2dc7..c28a9429dd 100644 --- a/apps/web/lib/csp.ts +++ b/apps/web/lib/csp.ts @@ -1,10 +1,11 @@ import type { IncomingMessage, OutgoingMessage } from "http"; +import type { NextRequest, NextResponse } from "next/server"; import { z } from "zod"; import { IS_PRODUCTION } from "@calcom/lib/constants"; import { WEBAPP_URL } from "@calcom/lib/constants"; -import { buildNonce } from "@lib/buildNonce"; +import { buildNonce } from "./buildNonce"; function getCspPolicy(nonce: string) { //TODO: Do we need to explicitly define it in turbo.json @@ -46,11 +47,12 @@ const isPagePathRequest = (url: URL) => { return !isNonPagePathPrefix.test(pathname) && !isFile.test(pathname); }; -export function csp(req: IncomingMessage | null, res: OutgoingMessage | null) { +export function csp(req: IncomingMessage | NextRequest | null, res: OutgoingMessage | NextResponse | null) { if (!req) { return { nonce: undefined }; } - const existingNonce = req.headers["x-nonce"]; + const existingNonce = "cache" in req ? req.headers.get("x-nonce") : req.headers["x-nonce"]; + if (existingNonce) { const existingNoneParsed = z.string().safeParse(existingNonce); return { nonce: existingNoneParsed.success ? existingNoneParsed.data : "" }; @@ -71,17 +73,28 @@ export function csp(req: IncomingMessage | null, res: OutgoingMessage | null) { } // Set x-nonce request header to be used by `getServerSideProps` or similar fns and `Document.getInitialProps` to read the nonce from // It is generated for all page requests but only used by pages that need CSP - req.headers["x-nonce"] = nonce; + + if ("cache" in req) { + req.headers.set("x-nonce", nonce); + } else { + req.headers["x-nonce"] = nonce; + } if (res) { - res.setHeader( - req.headers["x-csp-enforce"] === "true" - ? "Content-Security-Policy" - : "Content-Security-Policy-Report-Only", - getCspPolicy(nonce) - .replace(/\s{2,}/g, " ") - .trim() - ); + const enforced = + "cache" in req ? req.headers.get("x-csp-enforce") === "true" : req.headers["x-csp-enforce"] === "true"; + + const name = enforced ? "Content-Security-Policy" : "Content-Security-Policy-Report-Only"; + + const value = getCspPolicy(nonce) + .replace(/\s{2,}/g, " ") + .trim(); + + if ("body" in res) { + res.headers.set(name, value); + } else { + res.setHeader(name, value); + } } return { nonce }; diff --git a/apps/web/middleware.ts b/apps/web/middleware.ts index 8f8b8da78f..b3fc0e2a83 100644 --- a/apps/web/middleware.ts +++ b/apps/web/middleware.ts @@ -5,10 +5,14 @@ import { NextResponse } from "next/server"; import { extendEventData, nextCollectBasicSettings } from "@calcom/lib/telemetry"; +import { csp } from "@lib/csp"; + const middleware: NextMiddleware = async (req) => { const url = req.nextUrl; const requestHeaders = new Headers(req.headers); + requestHeaders.set("x-url", req.url); + if (!url.pathname.startsWith("/api")) { // // NOTE: When tRPC hits an error a 500 is returned, when this is received @@ -32,6 +36,18 @@ const middleware: NextMiddleware = async (req) => { } const res = routingForms.handle(url); + + const { nonce } = csp(req, res ?? null); + + if (!process.env.CSP_POLICY) { + req.headers.set("x-csp", "not-opted-in"); + } else if (!req.headers.get("x-csp")) { + // If x-csp not set by gSSP, then it's initialPropsOnly + req.headers.set("x-csp", "initialPropsOnly"); + } else { + req.headers.set("x-csp", nonce ?? ""); + } + if (res) { return res; } diff --git a/apps/web/pages/event-types/index.tsx b/apps/web/pages/event-types/index.tsx index 4d68795e92..468e1fde5d 100644 --- a/apps/web/pages/event-types/index.tsx +++ b/apps/web/pages/event-types/index.tsx @@ -386,7 +386,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL type.metadata?.managedEventConfig !== undefined && type.schedulingType !== SchedulingType.MANAGED; return (