fix(cms): thread a per-request nonce through the admin CSP

The prod CSP emitted script-src 'strict-dynamic' with no nonce seed,
blocking every Payload admin script (A8). Reuse the shared nonce-based
withSecurityHeaders: Payload admin pages are always dynamically
rendered, so Next propagates the nonce read from the forwarded
request's CSP header onto the admin's scripts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-10 17:25:04 +02:00
parent 32163312e3
commit cd61b31e65
2 changed files with 34 additions and 15 deletions

View File

@@ -55,10 +55,12 @@ describe("cms middleware", () => {
}
});
it("does not set a nonce header", () => {
it("sets a per-request nonce header on the response", () => {
middleware(makeRequest());
expect(mock._store.has("x-nonce")).toBe(false);
const nonce = mock._store.get("x-nonce");
expect(nonce).toBeDefined();
expect((nonce as string).length).toBeGreaterThan(0);
});
it("CSP is permissive in development mode", () => {
@@ -70,12 +72,31 @@ describe("cms middleware", () => {
expect(csp).toContain("'unsafe-inline'");
});
it("CSP uses strict-dynamic in production mode", () => {
it("production CSP uses strict-dynamic seeded with the nonce", () => {
vi.stubEnv("NODE_ENV", "production");
middleware(makeRequest());
const csp = mock._store.get("Content-Security-Policy");
const nonce = mock._store.get("x-nonce");
expect(csp).toContain("'strict-dynamic'");
expect(csp).toContain(`'nonce-${nonce}'`);
});
it("forwards the CSP + nonce on the request headers so Next can propagate it to Payload's scripts", () => {
vi.stubEnv("NODE_ENV", "production");
middleware(makeRequest());
const call = vi.mocked(NextResponse.next).mock.calls[0] as [
{ request?: { headers?: Headers } } | undefined,
];
const requestHeaders = call[0]?.request?.headers;
const requestCsp = requestHeaders?.get("Content-Security-Policy");
const nonce = requestHeaders?.get("x-nonce");
expect(requestCsp).toBeTruthy();
expect(nonce).toBeTruthy();
expect(requestCsp).toContain(`'nonce-${nonce}'`);
expect(requestCsp).toBe(mock._store.get("Content-Security-Policy"));
});
});