fix: implement @next-safe/middleware for CSP (qa-requested)

- Refactor src/middleware.ts to use chainMatch() and csp() from @next-safe/middleware
- Replace manual response.headers.set() approach with @next-safe/middleware composition
- Use chain() to properly compose i18n middleware with security middleware
- Fixes Next.js rewrite limitation where headers set on rewrite responses don't propagate
- Update next.config.ts comment to reflect correct CSP implementation

Implements QA Session 2 fix request (previously not implemented correctly).

Fixes QA rejections from Sessions 1, 2, and 3: security headers not appearing due to Next.js rewrite edge case (GitHub Issue #70515).

Using industry-standard @next-safe/middleware package as documented solution for combining next-intl with security headers.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-25 13:24:50 +01:00
co-authored by Claude Sonnet 4.5
parent 4ab5f2ddfe
commit a2b919c6ca
2 changed files with 22 additions and 27 deletions
+1
View File
@@ -55,6 +55,7 @@ const nextConfig: NextConfig = {
value: 'geolocation=(), microphone=(), camera=(), payment=(), usb=()',
},
// Content-Security-Policy is handled by @next-safe/middleware in src/middleware.ts
// Other security headers (HSTS, Referrer-Policy, Permissions-Policy) are static and configured here
],
},
{
+20 -26
View File
@@ -1,6 +1,6 @@
import { chain, chainMatch, isPageRequest, csp } from '@next-safe/middleware';
import createMiddleware from 'next-intl/middleware';
import { locales, defaultLocale } from './i18n/config';
import { NextRequest } from 'next/server';
const handleI18nRouting = createMiddleware({
locales,
@@ -8,34 +8,28 @@ const handleI18nRouting = createMiddleware({
localePrefix: 'always',
});
export default function middleware(request: NextRequest) {
// Handle i18n routing first
const response = handleI18nRouting(request);
// Define CSP using @next-safe/middleware
const securityMiddleware = csp({
directives: {
'default-src': ["'self'"],
'script-src': ["'self'", "'unsafe-inline'", "'unsafe-eval'"],
'style-src': ["'self'", "'unsafe-inline'", 'https://fonts.googleapis.com'],
'font-src': ["'self'", 'https://fonts.gstatic.com', 'data:'],
'img-src': ["'self'", 'data:', 'blob:', 'https://mxadgucxhmstlzsbgmoz.supabase.co'],
'connect-src': ["'self'", 'https://mxadgucxhmstlzsbgmoz.supabase.co'],
'frame-ancestors': ["'self'"],
'base-uri': ["'self'"],
'form-action': ["'self'"],
},
});
// Add security headers to the response
response.headers.set(
'Content-Security-Policy',
"default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com data:; img-src 'self' data: blob: https://mxadgucxhmstlzsbgmoz.supabase.co; connect-src 'self' https://mxadgucxhmstlzsbgmoz.supabase.co; frame-ancestors 'self'; base-uri 'self'; form-action 'self'"
// Use chain to combine i18n middleware with security middleware
// First run i18n, then apply CSP only on page requests
export default chain(
handleI18nRouting,
chainMatch(isPageRequest)(securityMiddleware)
);
response.headers.set(
'Strict-Transport-Security',
'max-age=31536000; includeSubDomains; preload'
);
response.headers.set(
'Referrer-Policy',
'strict-origin-when-cross-origin'
);
response.headers.set(
'Permissions-Policy',
'geolocation=(), microphone=(), camera=(), payment=(), usb=()'
);
return response;
}
export const config = {
matcher: [
'/',