mirror of
https://github.com/BigBodyCobain/Shadowbroker.git
synced 2026-08-08 03:38:53 +02:00
Align CSP tests with hydration-safe policy
This commit is contained in:
@@ -2,8 +2,8 @@
|
|||||||
* Phase 5F-A: CSP nonce plumbing tests.
|
* Phase 5F-A: CSP nonce plumbing tests.
|
||||||
*
|
*
|
||||||
* Validates:
|
* Validates:
|
||||||
* 1. Nonce appears in document CSP header
|
* 1. Document CSP remains hydration-safe for the Next.js runtime
|
||||||
* 2. Nonce differs across repeated requests
|
* 2. CSP is deterministic across repeated requests
|
||||||
* 3. next.config.ts no longer owns a static CSP header
|
* 3. next.config.ts no longer owns a static CSP header
|
||||||
* 4. Middleware does not break API/static routes (matcher exclusion)
|
* 4. Middleware does not break API/static routes (matcher exclusion)
|
||||||
* 5. Google Fonts domains are preserved in CSP
|
* 5. Google Fonts domains are preserved in CSP
|
||||||
@@ -41,58 +41,46 @@ function matcherExcludes(path: string): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// 1. Nonce appears in document CSP header
|
// 1. Document CSP remains hydration-safe
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
describe('nonce in CSP header', () => {
|
describe('hydration-safe CSP header', () => {
|
||||||
it('CSP header contains a nonce-<value> token in script-src', () => {
|
it('CSP header does not put nonce tokens in script-src', () => {
|
||||||
const csp = getCsp();
|
const csp = getCsp();
|
||||||
expect(csp).toMatch(/'nonce-[A-Za-z0-9+/=]+'/) ;
|
expect(csp).not.toMatch(/'nonce-[A-Za-z0-9+/=]+'/);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('nonce value is a base64-encoded UUID', () => {
|
it('script-src keeps the inline compatibility fallback required by Next hydration', () => {
|
||||||
const csp = getCsp();
|
const csp = getCsp();
|
||||||
const match = csp.match(/'nonce-([A-Za-z0-9+/=]+)'/);
|
expect(csp).toMatch(/script-src [^;]*'unsafe-inline'/);
|
||||||
expect(match).not.toBeNull();
|
|
||||||
const decoded = Buffer.from(match![1], 'base64').toString();
|
|
||||||
// crypto.randomUUID() produces 8-4-4-4-12 hex with dashes
|
|
||||||
expect(decoded).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-/);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('x-nonce request header is set on the response', () => {
|
it('middleware still returns a CSP header for document requests', () => {
|
||||||
const res = callMiddleware();
|
const csp = getCsp();
|
||||||
// NextResponse.next({ request: { headers } }) merges into request headers.
|
expect(csp).toContain("default-src 'self'");
|
||||||
// The CSP nonce in the header must match the one forwarded to server components.
|
expect(csp).toContain("script-src 'self'");
|
||||||
const csp = res.headers.get('Content-Security-Policy') ?? '';
|
|
||||||
const nonceInCsp = csp.match(/'nonce-([A-Za-z0-9+/=]+)'/)?.[1];
|
|
||||||
expect(nonceInCsp).toBeTruthy();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// 2. Nonce differs across repeated requests
|
// 2. CSP is deterministic across repeated requests
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
describe('nonce uniqueness', () => {
|
describe('CSP stability', () => {
|
||||||
it('two sequential requests produce different nonces', () => {
|
it('two sequential requests produce the same document CSP', () => {
|
||||||
const csp1 = getCsp();
|
const csp1 = getCsp();
|
||||||
const csp2 = getCsp();
|
const csp2 = getCsp();
|
||||||
const nonce1 = csp1.match(/'nonce-([A-Za-z0-9+/=]+)'/)?.[1];
|
expect(csp1).toBe(csp2);
|
||||||
const nonce2 = csp2.match(/'nonce-([A-Za-z0-9+/=]+)'/)?.[1];
|
|
||||||
expect(nonce1).toBeTruthy();
|
|
||||||
expect(nonce2).toBeTruthy();
|
|
||||||
expect(nonce1).not.toBe(nonce2);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('ten requests produce ten distinct nonces', () => {
|
it('ten requests do not introduce nonce-bearing CSP variants', () => {
|
||||||
const nonces = new Set<string>();
|
const csps = new Set<string>();
|
||||||
for (let i = 0; i < 10; i++) {
|
for (let i = 0; i < 10; i++) {
|
||||||
const csp = getCsp();
|
const csp = getCsp();
|
||||||
const nonce = csp.match(/'nonce-([A-Za-z0-9+/=]+)'/)?.[1];
|
expect(csp).not.toMatch(/'nonce-[A-Za-z0-9+/=]+'/);
|
||||||
expect(nonce).toBeTruthy();
|
csps.add(csp);
|
||||||
nonces.add(nonce!);
|
|
||||||
}
|
}
|
||||||
expect(nonces.size).toBe(10);
|
expect(csps.size).toBe(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -185,8 +173,9 @@ describe('production CSP directive completeness', () => {
|
|||||||
expect(csp).toContain("default-src 'self'");
|
expect(csp).toContain("default-src 'self'");
|
||||||
});
|
});
|
||||||
|
|
||||||
it('has script-src with nonce', () => {
|
it('has script-src with hydration compatibility fallback', () => {
|
||||||
expect(csp).toMatch(/script-src [^;]*'nonce-/);
|
expect(csp).toMatch(/script-src [^;]*'unsafe-inline'/);
|
||||||
|
expect(csp).not.toMatch(/script-src [^;]*'nonce-/);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('has style-src with unsafe-inline and fonts.googleapis.com', () => {
|
it('has style-src with unsafe-inline and fonts.googleapis.com', () => {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* Phase 5F-B: Production script-src nonce hardening tests.
|
* Phase 5F-B: Production script-src nonce hardening tests.
|
||||||
*
|
*
|
||||||
* Validates:
|
* Validates:
|
||||||
* 1. Production CSP preserves nonce-based script execution with a compatibility
|
* 1. Production CSP preserves hydration-safe script execution with a compatibility
|
||||||
* inline fallback required by the Next.js production runtime
|
* inline fallback required by the Next.js production runtime
|
||||||
* 2. Dev CSP retains 'unsafe-inline' and 'unsafe-eval'
|
* 2. Dev CSP retains 'unsafe-inline' and 'unsafe-eval'
|
||||||
* 3. Unchanged directives (style-src, font-src, worker-src, etc.) intact
|
* 3. Unchanged directives (style-src, font-src, worker-src, etc.) intact
|
||||||
@@ -42,7 +42,7 @@ function matcherExcludes(path: string): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// 1. Production CSP keeps nonce hardening without blocking Next hydration
|
// 1. Production CSP stays hardened without blocking Next hydration
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
describe('production script-src hardening', () => {
|
describe('production script-src hardening', () => {
|
||||||
@@ -63,9 +63,9 @@ describe('production script-src hardening', () => {
|
|||||||
expect(scriptSrc).not.toContain("'unsafe-eval'");
|
expect(scriptSrc).not.toContain("'unsafe-eval'");
|
||||||
});
|
});
|
||||||
|
|
||||||
it('production script-src contains nonce', () => {
|
it('production script-src does not contain nonce until all Next inline scripts are wired', () => {
|
||||||
const scriptSrc = getDirective('script-src');
|
const scriptSrc = getDirective('script-src');
|
||||||
expect(scriptSrc).toMatch(/'nonce-[A-Za-z0-9+/=]+'/);
|
expect(scriptSrc).not.toMatch(/'nonce-[A-Za-z0-9+/=]+'/);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('production script-src contains self and blob:', () => {
|
it('production script-src contains self and blob:', () => {
|
||||||
@@ -106,9 +106,9 @@ describe('dev script-src allowances', () => {
|
|||||||
expect(scriptSrc).toContain("'unsafe-eval'");
|
expect(scriptSrc).toContain("'unsafe-eval'");
|
||||||
});
|
});
|
||||||
|
|
||||||
it('dev script-src still contains nonce', () => {
|
it('dev script-src also omits nonce to match production hydration behavior', () => {
|
||||||
const scriptSrc = getDirective('script-src');
|
const scriptSrc = getDirective('script-src');
|
||||||
expect(scriptSrc).toMatch(/'nonce-[A-Za-z0-9+/=]+'/);
|
expect(scriptSrc).not.toMatch(/'nonce-[A-Za-z0-9+/=]+'/);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('dev connect-src includes localhost backends', () => {
|
it('dev connect-src includes localhost backends', () => {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
function buildCsp(nonce: string): string {
|
function buildCsp(_nonce: string): string {
|
||||||
const isDev = process.env.NODE_ENV !== 'production';
|
const isDev = process.env.NODE_ENV !== 'production';
|
||||||
const directives = [
|
const directives = [
|
||||||
"default-src 'self'",
|
"default-src 'self'",
|
||||||
|
|||||||
Reference in New Issue
Block a user