diff --git a/__checks__/README.md b/__checks__/README.md index 7063c1b63a..10626ac75b 100644 --- a/__checks__/README.md +++ b/__checks__/README.md @@ -1,4 +1,6 @@ # Checkly Tests Run as `yarn checkly test` +Optionally, run as `yarn checkly test --record` to be able to get detailed debugging view in Checkly UI +Run the tests for a particylar file as `yarn checkly test {filePattern}` Deploy the tests as `yarn checkly deploy` diff --git a/__checks__/csp-login.spec.ts b/__checks__/csp-login.spec.ts new file mode 100644 index 0000000000..5b1ba2d104 --- /dev/null +++ b/__checks__/csp-login.spec.ts @@ -0,0 +1,22 @@ +import { test, expect } from "@playwright/test"; + +test.describe("CSP Headers", () => { + test("Login page should have CSP header with nonce", async ({ page }) => { + const targetUrl = process.env.ENVIRONMENT_URL || "https://app.cal.com"; + + const response = await page.goto(`${targetUrl}/auth/login`); + + expect(response?.status()).toBe(200); + + const cspHeader = response?.headers()["content-security-policy"]; + expect(cspHeader).toBeTruthy(); + + // Verify nonce is present in CSP header + const nonceMatch = cspHeader?.match(/'nonce-([^']+)'/); + expect(nonceMatch).toBeTruthy(); + expect(nonceMatch![1]).toHaveLength(24); + expect(nonceMatch![1]).toMatch(/==$/); + + await page.screenshot({ path: "screenshot.jpg" }); + }); +}); diff --git a/apps/web/app/(booking-page-wrapper)/layout.tsx b/apps/web/app/(booking-page-wrapper)/layout.tsx index b2a76da168..ec7d4ee31d 100644 --- a/apps/web/app/(booking-page-wrapper)/layout.tsx +++ b/apps/web/app/(booking-page-wrapper)/layout.tsx @@ -4,7 +4,7 @@ import PageWrapper from "@components/PageWrapperAppDir"; export default async function BookingPageWrapperLayout({ children }: { children: React.ReactNode }) { const h = await headers(); - const nonce = h.get("x-nonce") ?? undefined; + const nonce = h.get("x-csp-nonce") ?? undefined; return ( <> diff --git a/apps/web/app/(use-page-wrapper)/layout.tsx b/apps/web/app/(use-page-wrapper)/layout.tsx index a7be94bd6a..878635c237 100644 --- a/apps/web/app/(use-page-wrapper)/layout.tsx +++ b/apps/web/app/(use-page-wrapper)/layout.tsx @@ -5,7 +5,7 @@ import PageWrapper from "@components/PageWrapperAppDir"; export default async function PageWrapperLayout({ children }: { children: React.ReactNode }) { const h = await headers(); - const nonce = h.get("x-nonce") ?? undefined; + const nonce = h.get("x-csp-nonce") ?? undefined; const headScript = process.env.NEXT_PUBLIC_HEAD_SCRIPTS; const bodyScript = process.env.NEXT_PUBLIC_BODY_SCRIPTS; diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index a54303ed3c..94044fd63b 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -96,7 +96,7 @@ const getInitialProps = async () => { export default async function RootLayout({ children }: { children: React.ReactNode }) { const h = await headers(); - const nonce = h.get("x-csp") ?? ""; + const nonce = h.get("x-csp-nonce") ?? ""; const { locale, direction, isEmbed, embedColorScheme } = await getInitialProps(); @@ -151,7 +151,7 @@ export default async function RootLayout({ children }: { children: React.ReactNo ]} /> - + {children} diff --git a/apps/web/app/not-found.tsx b/apps/web/app/not-found.tsx index fbd9529d92..fd9c410976 100644 --- a/apps/web/app/not-found.tsx +++ b/apps/web/app/not-found.tsx @@ -21,7 +21,7 @@ export const generateMetadata = async () => { const ServerPage = async () => { const h = await headers(); - const nonce = h.get("x-nonce") ?? undefined; + const nonce = h.get("x-csp-nonce") ?? undefined; const host = h.get("x-forwarded-host") ?? ""; return ( diff --git a/apps/web/app/providers.tsx b/apps/web/app/providers.tsx index 84e1b21e7d..5a7ab55124 100644 --- a/apps/web/app/providers.tsx +++ b/apps/web/app/providers.tsx @@ -13,14 +13,15 @@ import PlainChat from "@lib/plain/dynamicProvider"; type ProvidersProps = { isEmbed: boolean; children: React.ReactNode; + nonce: string | undefined; }; -export function Providers({ isEmbed, children }: ProvidersProps) { +export function Providers({ isEmbed, children, nonce }: ProvidersProps) { const isBookingPage = useIsBookingPage(); return ( - {!isBookingPage ? : null} + {!isBookingPage ? : null} {!isEmbed && !isBookingPage && } {/* @ts-expect-error FIXME remove this comment when upgrading typescript to v5 */} diff --git a/apps/web/components/PageWrapper.tsx b/apps/web/components/PageWrapper.tsx index b97bd6b86c..957b388152 100644 --- a/apps/web/components/PageWrapper.tsx +++ b/apps/web/components/PageWrapper.tsx @@ -44,17 +44,8 @@ function PageWrapper(props: AppProps) { pageStatus = "403"; } - // On client side don't let nonce creep into DOM - // It also avoids hydration warning that says that Client has the nonce value but server has "" because browser removes nonce attributes before DOM is built - // See https://github.com/kentcdodds/nonce-hydration-issues - // Set "" only if server had it set otherwise keep it undefined because server has to match with client to avoid hydration error - const nonce = typeof window !== "undefined" ? (pageProps.nonce ? "" : undefined) : pageProps.nonce; const providerProps = { ...props, - pageProps: { - ...props.pageProps, - nonce, - }, }; // Use the layout defined at the page level, if available const getLayout = Component.getLayout ?? ((page) => page); @@ -79,7 +70,6 @@ function PageWrapper(props: AppProps) { {...seoConfig.defaultNextSeo} />