fix(core-shared): set CSP on forwarded request headers for nonce
Next.js only injects the nonce into its own scripts when it can read it from the request's Content-Security-Policy header. Setting the CSP only on the response left hydration scripts un-nonced in production (A9). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -90,6 +90,34 @@ describe("withSecurityHeaders", () => {
|
|||||||
expect(call[0]?.request?.headers?.get("x-nonce")).toBeTruthy();
|
expect(call[0]?.request?.headers?.get("x-nonce")).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("sets the CSP on the forwarded request headers (Next reads the nonce from there)", () => {
|
||||||
|
withSecurityHeaders(makeRequest());
|
||||||
|
|
||||||
|
const call = vi.mocked(NextResponse.next).mock.calls[0] as [
|
||||||
|
{ request?: { headers?: Headers } } | undefined,
|
||||||
|
];
|
||||||
|
const requestCsp = call[0]?.request?.headers?.get(
|
||||||
|
"Content-Security-Policy",
|
||||||
|
);
|
||||||
|
expect(requestCsp).toBeTruthy();
|
||||||
|
expect(requestCsp).toBe(mock._store.get("Content-Security-Policy"));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("request-header CSP carries the nonce in production mode", () => {
|
||||||
|
vi.stubEnv("NODE_ENV", "production");
|
||||||
|
|
||||||
|
withSecurityHeaders(makeRequest());
|
||||||
|
|
||||||
|
const call = vi.mocked(NextResponse.next).mock.calls[0] as [
|
||||||
|
{ request?: { headers?: Headers } } | undefined,
|
||||||
|
];
|
||||||
|
const requestCsp = call[0]?.request?.headers?.get(
|
||||||
|
"Content-Security-Policy",
|
||||||
|
);
|
||||||
|
const nonce = call[0]?.request?.headers?.get("x-nonce");
|
||||||
|
expect(requestCsp).toContain(`'nonce-${nonce}'`);
|
||||||
|
});
|
||||||
|
|
||||||
it("returns the NextResponse from NextResponse.next", () => {
|
it("returns the NextResponse from NextResponse.next", () => {
|
||||||
const result = withSecurityHeaders(makeRequest());
|
const result = withSecurityHeaders(makeRequest());
|
||||||
|
|
||||||
|
|||||||
@@ -7,9 +7,16 @@ export function withSecurityHeaders(request: NextRequest): NextResponse {
|
|||||||
const nonce = generateNonce();
|
const nonce = generateNonce();
|
||||||
const mode = process.env.NODE_ENV === "production" ? "prod" : "dev";
|
const mode = process.env.NODE_ENV === "production" ? "prod" : "dev";
|
||||||
const secHeaders = buildSecurityHeaders({ mode, nonce });
|
const secHeaders = buildSecurityHeaders({ mode, nonce });
|
||||||
|
const csp = secHeaders["Content-Security-Policy"];
|
||||||
|
|
||||||
const requestHeaders = new Headers(request.headers);
|
const requestHeaders = new Headers(request.headers);
|
||||||
requestHeaders.set("x-nonce", nonce);
|
requestHeaders.set("x-nonce", nonce);
|
||||||
|
// Next.js only propagates a nonce to its own <script> tags when it can
|
||||||
|
// read it from the *request's* Content-Security-Policy header, so the CSP
|
||||||
|
// must be set on the forwarded request headers — not just the response.
|
||||||
|
if (csp) {
|
||||||
|
requestHeaders.set("Content-Security-Policy", csp);
|
||||||
|
}
|
||||||
|
|
||||||
const response = NextResponse.next({
|
const response = NextResponse.next({
|
||||||
request: { headers: requestHeaders },
|
request: { headers: requestHeaders },
|
||||||
|
|||||||
Reference in New Issue
Block a user