Files
cloudflare__vinext/tests/csp.test.ts
T
Divanshu Chauhan 067a7c6000 fix(csp): match script-src-elem/attr when extracting script nonce (#2049)
Select the CSP nonce directive via startsWith("script-src") /
startsWith("default-src"), matching Next.js get-script-nonce-from-header.
Previously the exact-match predicate skipped script-src-elem/script-src-attr
directives, so a CSP declaring the nonce only on script-src-elem fell through
to default-src and SSR scripts could be emitted without the expected nonce
and blocked by the browser CSP.

Closes #1989
2026-06-15 23:39:55 +01:00

128 lines
4.8 KiB
TypeScript

import { describe, expect, it } from "vite-plus/test";
import {
getScriptNonceFromHeader,
getScriptNonceFromHeaderSources,
getScriptNonceFromNodeHeaderSources,
getScriptNonceFromHeaders,
} from "../packages/vinext/src/server/csp.js";
describe("CSP nonce helpers", () => {
it("extracts the nonce from script-src", () => {
expect(
getScriptNonceFromHeader("default-src 'self'; script-src 'self' 'nonce-test-nonce';"),
).toBe("test-nonce");
});
it("falls back to default-src when script-src is absent", () => {
expect(getScriptNonceFromHeader("default-src 'nonce-test-nonce'")).toBe("test-nonce");
});
it("prefers script-src over default-src when both are present", () => {
expect(
getScriptNonceFromHeader(
"default-src 'nonce-default'; script-src 'self' 'nonce-script'; style-src 'self'",
),
).toBe("script");
});
it("extracts the nonce from script-src-elem when no plain script-src is present", () => {
expect(getScriptNonceFromHeader("script-src-elem 'nonce-abc'; default-src 'self'")).toBe("abc");
});
it("extracts the nonce from script-src-attr when no plain script-src is present", () => {
expect(getScriptNonceFromHeader("script-src-attr 'nonce-attr'; default-src 'self'")).toBe(
"attr",
);
});
it("matches the first script-src* directive (Next.js startsWith parity)", () => {
expect(
getScriptNonceFromHeader(
"script-src-elem 'nonce-element'; script-src-attr 'nonce-attr'; script-src 'nonce-script'",
),
).toBe("element");
});
it("matches the first default-src* directive when falling back (Next.js startsWith parity)", () => {
expect(
getScriptNonceFromHeader(
"default-src-elem 'nonce-element'; default-src-attr 'nonce-attr'; default-src 'nonce-default'",
),
).toBe("element");
});
it("only reads the first script-src* directive and does not scan later script-src directives", () => {
// Next.js selects a single directive via find(startsWith('script-src')) and
// extracts the nonce only from that directive, so a nonce on a later plain
// script-src is not used.
expect(
getScriptNonceFromHeader("script-src-elem 'self'; script-src 'nonce-x'"),
).toBeUndefined();
});
it("parses the first matching nonce across extra whitespace and additional nonces", () => {
expect(
getScriptNonceFromHeader(
" script-src 'self' 'nonce-first' 'nonce-second' 'strict-dynamic' ",
),
).toBe("first");
});
it("returns undefined when script-src/default-src does not contain a valid nonce", () => {
expect(getScriptNonceFromHeader("script-src 'nonce-'")).toBeUndefined();
expect(getScriptNonceFromHeader("style-src 'nonce-test-nonce'")).toBeUndefined();
expect(getScriptNonceFromHeader("")).toBeUndefined();
});
it("reads Content-Security-Policy-Report-Only when CSP is absent", () => {
const headers = new Headers({
"content-security-policy-report-only": "script-src 'nonce-test-nonce' 'strict-dynamic';",
});
expect(getScriptNonceFromHeaders(headers)).toBe("test-nonce");
});
it("prefers request CSP over fallback header sources", () => {
const requestHeaders = new Headers({
"content-security-policy": "script-src 'nonce-request-nonce' 'strict-dynamic';",
});
const middlewareHeaders = new Headers({
"content-security-policy": "script-src 'nonce-response-nonce' 'strict-dynamic';",
});
expect(getScriptNonceFromHeaderSources(requestHeaders, middlewareHeaders)).toBe(
"request-nonce",
);
});
it("falls back to later header sources when request headers do not contain a nonce", () => {
const requestHeaders = new Headers({
"content-security-policy": "script-src 'self' 'strict-dynamic';",
});
const fallbackHeaders = new Headers({
"content-security-policy-report-only": "script-src 'nonce-request-nonce' 'strict-dynamic';",
});
expect(getScriptNonceFromHeaderSources(requestHeaders, fallbackHeaders)).toBe("request-nonce");
});
it("reads the first nonce across Node request/response header sources without allocating Headers", () => {
const requestHeaders = {
"content-security-policy": "script-src 'self' 'strict-dynamic';",
};
const responseHeaders = {
"content-security-policy-report-only": "script-src 'nonce-response-nonce' 'strict-dynamic';",
};
expect(getScriptNonceFromNodeHeaderSources(requestHeaders, responseHeaders)).toBe(
"response-nonce",
);
});
it("throws on HTML-escape characters using Next.js-compatible messaging", () => {
expect(() => getScriptNonceFromHeader("script-src 'nonce-bad&nonce'")).toThrow(
"Nonce value from Content-Security-Policy contained HTML escape characters.\nLearn more: https://nextjs.org/docs/messages/nonce-contained-invalid-characters",
);
});
});