import { createRequire } from "node:module"; import { describe, expect, it } from "vitest"; import { getCspHeader } from "@/lib/csp"; const require = createRequire(import.meta.url); const config = require("./next.config.js") as { headers: () => Promise< Array<{ headers: Array<{ key: string; value: string }> }> >; }; const POSTHOG_WILDCARD = "https://*.posthog.com"; const ENABLED_POSTHOG_CONFIG = { cloudEnabled: true, posthogEnabled: true, posthogKey: "phc_key", posthogIngestionHost: "https://eu.i.posthog.com", posthogUiHost: null, posthogToolbarEnabled: false, }; const BASELINE_CSP = { "default-src": ["'self'"], "script-src": [ "'self'", "'unsafe-inline'", "'unsafe-eval'", "https://js.stripe.com", "https://www.googletagmanager.com", "https://browser.sentry-cdn.com", ], "connect-src": [ "'self'", "https://api.iconify.design", "https://api.simplesvg.com", "https://api.unisvg.com", "https://js.stripe.com", "https://www.googletagmanager.com", "https://*.sentry.io", "https://*.ingest.sentry.io", ], "img-src": [ "'self'", "https://www.google-analytics.com", "https://www.googletagmanager.com", ], "font-src": ["'self'"], "style-src": ["'self'", "'unsafe-inline'"], "frame-src": [ "'self'", "https://js.stripe.com", "https://www.googletagmanager.com", ], "frame-ancestors": ["'none'"], } as const; const getStaticCsp = async () => { const rules = await config.headers(); return rules[0]?.headers.find(({ key }) => key === "Content-Security-Policy"); }; const parseCsp = (value: string) => { return Object.fromEntries( value .split(";") .map((entry) => entry.trim().split(/\s+/)) .filter(([name]) => name) .map(([name, ...sources]) => [name, sources]), ) as Record; }; describe("PostHog Content Security Policy", () => { it("does not configure CSP through static Next headers", async () => { // When const staticCsp = await getStaticCsp(); // Then expect(staticCsp).toBeUndefined(); }); it("omits PostHog permissions from the baseline request CSP", () => { // When const csp = parseCsp( getCspHeader({ cloudEnabled: false, posthogEnabled: false, posthogKey: null, posthogIngestionHost: null, posthogUiHost: null, posthogToolbarEnabled: false, }), ); // Then expect(csp).toEqual(BASELINE_CSP); expect(Object.values(csp).flat()).not.toContain(POSTHOG_WILDCARD); }); it("adds PostHog permissions only for a fully enabled Cloud request", () => { // When const csp = parseCsp(getCspHeader(ENABLED_POSTHOG_CONFIG)); // Then expect(csp["script-src"]).toContain(POSTHOG_WILDCARD); expect(csp["connect-src"]).toContain(POSTHOG_WILDCARD); expect(csp["img-src"]).toContain(POSTHOG_WILDCARD); expect(csp["frame-src"]).toContain(POSTHOG_WILDCARD); expect(csp["font-src"]).not.toContain(POSTHOG_WILDCARD); expect(csp["style-src"]).not.toContain(POSTHOG_WILDCARD); expect(csp["media-src"]).toBeUndefined(); expect(csp["worker-src"]).toBeUndefined(); expect(csp["frame-ancestors"]).toEqual(["'none'"]); expect(csp["default-src"]).not.toContain(POSTHOG_WILDCARD); }); it("adds Toolbar permissions for a fully enabled Cloud development request", () => { // Given const toolbarConfig = { ...ENABLED_POSTHOG_CONFIG, posthogToolbarEnabled: true, }; // When const csp = parseCsp(getCspHeader(toolbarConfig)); // Then expect(csp["style-src"]).toContain(POSTHOG_WILDCARD); expect(csp["font-src"]).toContain(POSTHOG_WILDCARD); expect(csp["media-src"]).toContain(POSTHOG_WILDCARD); expect(csp["worker-src"]).toEqual(["'self'", "blob:", "data:"]); expect(csp["frame-ancestors"]).toEqual(["'self'", POSTHOG_WILDCARD]); }); it("allows the resolved UI origin for a self-hosted Toolbar", () => { // Given const selfHostedUiOrigin = "https://posthog.internal.example"; const toolbarConfig = { ...ENABLED_POSTHOG_CONFIG, posthogIngestionHost: `${selfHostedUiOrigin}/ingest`, posthogUiHost: `${selfHostedUiOrigin}/app/`, posthogToolbarEnabled: true, }; // When const csp = parseCsp(getCspHeader(toolbarConfig)); // Then expect(csp["script-src"]).toContain(selfHostedUiOrigin); expect(csp["connect-src"]).toContain(selfHostedUiOrigin); expect(csp["img-src"]).toContain(selfHostedUiOrigin); expect(csp["style-src"]).toContain(selfHostedUiOrigin); expect(csp["font-src"]).toContain(selfHostedUiOrigin); expect(csp["media-src"]).toContain(selfHostedUiOrigin); expect(csp["frame-src"]).toContain(selfHostedUiOrigin); expect(csp["frame-ancestors"]).toContain(selfHostedUiOrigin); }); it("does not add a non-HTTP PostHog UI host to the CSP", () => { // Given const unsafeUiHost = "data:text/plain,toolbar"; const toolbarConfig = { ...ENABLED_POSTHOG_CONFIG, posthogUiHost: unsafeUiHost, posthogToolbarEnabled: true, }; // When const csp = parseCsp(getCspHeader(toolbarConfig)); // Then expect(Object.values(csp).flat()).not.toContain(unsafeUiHost); expect(Object.values(csp).flat()).not.toContain("null"); }); it("keeps Toolbar permissions closed when PostHog is not fully enabled", () => { // Given const toolbarConfig = { ...ENABLED_POSTHOG_CONFIG, posthogEnabled: false, posthogToolbarEnabled: true, }; // When const csp = parseCsp(getCspHeader(toolbarConfig)); // Then expect(Object.values(csp).flat()).not.toContain(POSTHOG_WILDCARD); expect(csp["media-src"]).toBeUndefined(); expect(csp["worker-src"]).toBeUndefined(); expect(csp["frame-ancestors"]).toEqual(["'none'"]); }); it.each([ ["Cloud is disabled", { cloudEnabled: false }], ["PostHog is disabled", { posthogEnabled: false }], ["the key is missing", { posthogKey: null }], ["the host is missing", { posthogIngestionHost: null }], ])("omits PostHog permissions when %s", (_case, override) => { // Given const config = { ...ENABLED_POSTHOG_CONFIG, ...override }; // When const csp = parseCsp(getCspHeader(config)); // Then expect(Object.values(csp).flat()).not.toContain(POSTHOG_WILDCARD); }); });