diff --git a/src/middleware.test.ts b/src/middleware.test.ts index 5cea4db..80add9c 100644 --- a/src/middleware.test.ts +++ b/src/middleware.test.ts @@ -42,11 +42,13 @@ describe('buildSecurityHeaders', () => { }); it('does not allow inline scripts', () => { - expect(headers['Content-Security-Policy']).not.toContain("'unsafe-inline' 'nonce-"); + // Script directive must not include 'unsafe-inline' — find it explicitly and assert. const scriptDirective = headers['Content-Security-Policy'] .split(';') .map(s => s.trim()) .find(s => s.startsWith('script-src')) ?? ''; + expect(scriptDirective).toContain("'self'"); expect(scriptDirective).not.toContain("'unsafe-inline'"); + expect(scriptDirective).not.toContain("'unsafe-eval'"); }); }); diff --git a/src/middleware.ts b/src/middleware.ts index f390a45..ab8f1c3 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -12,12 +12,16 @@ export function buildSecurityHeaders(): Record { const csp = [ "default-src 'self'", "img-src 'self' data:", + // Astro's scoped-style system injects inline style attributes at build time; + // 'unsafe-inline' here is required until we migrate to a hash/nonce strategy. "style-src 'self' 'unsafe-inline'", "font-src 'self'", "script-src 'self'", "connect-src 'self'", "frame-ancestors 'none'", "base-uri 'self'", + // No forms on this marketing site today (all auth redirects go to auth.cameleer.io + // as plain navigations). If a future form is added, relax to 'self' or an allow-list. "form-action 'none'", "object-src 'none'", ].join('; ');