feat(cms): wire security headers middleware
Add Next.js middleware.ts to apps/cms that applies all six security headers on every response using the framework-agnostic buildSecurityHeaders builder. No nonce is generated or forwarded — the CMS is server-side only so CSP nonces are not required. Includes a test suite mirroring the web-next pattern that asserts all six headers are set, no x-nonce is emitted, and CSP mode switches correctly between dev (unsafe-inline) and prod (strict-dynamic). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
81
apps/cms/src/middleware.test.ts
Normal file
81
apps/cms/src/middleware.test.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
|
||||
const responseMock = vi.hoisted(() => {
|
||||
function makeResponseMock() {
|
||||
const store = new Map<string, string>();
|
||||
return {
|
||||
_store: store,
|
||||
headers: {
|
||||
set: vi.fn((k: string, v: string) => store.set(k, v)),
|
||||
get: vi.fn((k: string) => store.get(k) ?? null),
|
||||
},
|
||||
};
|
||||
}
|
||||
return { makeResponseMock };
|
||||
});
|
||||
|
||||
vi.mock("next/server", () => ({
|
||||
NextResponse: {
|
||||
next: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
import type { NextRequest } from "next/server";
|
||||
import { NextResponse } from "next/server";
|
||||
import { middleware } from "../middleware";
|
||||
|
||||
const ALL_SIX_HEADERS = [
|
||||
"Strict-Transport-Security",
|
||||
"X-Frame-Options",
|
||||
"X-Content-Type-Options",
|
||||
"Referrer-Policy",
|
||||
"Permissions-Policy",
|
||||
"Content-Security-Policy",
|
||||
] as const;
|
||||
|
||||
function makeRequest(): NextRequest {
|
||||
return { headers: new Headers() } as unknown as NextRequest;
|
||||
}
|
||||
|
||||
describe("cms middleware", () => {
|
||||
let mock: ReturnType<typeof responseMock.makeResponseMock>;
|
||||
|
||||
beforeEach(() => {
|
||||
mock = responseMock.makeResponseMock();
|
||||
vi.mocked(NextResponse.next).mockReturnValue(
|
||||
mock as unknown as ReturnType<typeof NextResponse.next>,
|
||||
);
|
||||
});
|
||||
|
||||
it("sets all six security headers on the response", () => {
|
||||
middleware(makeRequest());
|
||||
|
||||
for (const header of ALL_SIX_HEADERS) {
|
||||
expect(mock._store.has(header)).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it("does not set a nonce header", () => {
|
||||
middleware(makeRequest());
|
||||
|
||||
expect(mock._store.has("x-nonce")).toBe(false);
|
||||
});
|
||||
|
||||
it("CSP is permissive in development mode", () => {
|
||||
vi.stubEnv("NODE_ENV", "development");
|
||||
|
||||
middleware(makeRequest());
|
||||
|
||||
const csp = mock._store.get("Content-Security-Policy");
|
||||
expect(csp).toContain("'unsafe-inline'");
|
||||
});
|
||||
|
||||
it("CSP uses strict-dynamic in production mode", () => {
|
||||
vi.stubEnv("NODE_ENV", "production");
|
||||
|
||||
middleware(makeRequest());
|
||||
|
||||
const csp = mock._store.get("Content-Security-Policy");
|
||||
expect(csp).toContain("'strict-dynamic'");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user