Files
prowler/ui/next.config.test.ts
T

219 lines
6.5 KiB
TypeScript

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://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<string, string[]>;
};
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);
});
});
it("allows configured private Registry images in the request CSP", () => {
const csp = parseCsp(
getCspHeader({
...ENABLED_POSTHOG_CONFIG,
registryImageOrigins: ["https://media.private.test"],
}),
);
expect(csp["img-src"]).toContain("https://media.private.test");
expect(csp["connect-src"]).not.toContain("https://media.private.test");
});