From 70984b9021e2ad298e0af29d8c515ec1b181bb0a Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Sun, 22 Feb 2026 17:11:15 +0100 Subject: [PATCH] feat(security): implement critical security headers and CSP allowlisting --- next-env.d.ts | 2 +- next.config.mjs | 53 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/next-env.d.ts b/next-env.d.ts index 9edff1c7..c4b7818f 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./.next/types/routes.d.ts"; +import "./.next/dev/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/next.config.mjs b/next.config.mjs index 95ba8e14..7c274fc8 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -21,6 +21,59 @@ const nextConfig = { }, }, output: 'standalone', + async headers() { + const umamiDomain = new URL(process.env.UMAMI_API_ENDPOINT || 'https://analytics.infra.mintel.me').origin; + const directusDomain = new URL(process.env.DIRECTUS_URL || 'https://cms.klz-cables.com').origin; + const imgproxyDomain = new URL(process.env.IMGPROXY_URL || 'https://img.infra.mintel.me').origin; + const glitchtipDomain = new URL(process.env.SENTRY_DSN ? new URL(process.env.SENTRY_DSN).origin : 'https://errors.infra.mintel.me').origin; + + const cspHeader = ` + default-src 'self'; + script-src 'self' 'unsafe-inline' 'unsafe-eval' ${umamiDomain}; + style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; + font-src 'self' https://fonts.gstatic.com; + img-src 'self' data: blob: ${imgproxyDomain} ${directusDomain}; + connect-src 'self' ${umamiDomain} ${glitchtipDomain} ${directusDomain}; + frame-src 'self'; + object-src 'none'; + base-uri 'self'; + form-action 'self'; + frame-ancestors 'none'; + upgrade-insecure-requests; + `.replace(/\s{2,}/g, ' ').trim(); + + return [ + { + source: '/:path*', + headers: [ + { + key: 'Content-Security-Policy', + value: cspHeader, + }, + { + key: 'Strict-Transport-Security', + value: 'max-age=63072000; includeSubDomains; preload', + }, + { + key: 'X-Frame-Options', + value: 'DENY', + }, + { + key: 'X-Content-Type-Options', + value: 'nosniff', + }, + { + key: 'Referrer-Policy', + value: 'strict-origin-when-cross-origin', + }, + { + key: 'Permissions-Policy', + value: 'camera=(), microphone=(), geolocation=(), interest-cohort=()', + }, + ], + }, + ]; + }, async redirects() { return [ // Blog redirects