apicore.encry.de lieferte bisher gar keine Security-Header aus (per curl bestaetigt) - CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, HSTS fehlten komplett, waehrend das Backend das laengst per helmet() macht. Ergaenzt fuer alle Routen. Bewusst ohne CSP-Nonce (haette dynamic rendering fuer jede Seite erzwungen und damit die bestehende statische Optimierung gekostet) - stattdessen script-src/style-src 'unsafe-inline', aber default-src/connect-src/ img-src/object-src/frame-ancestors/form-action/base-uri strikt eingeschraenkt. Lokal mit dem echten Production-Build (standalone server + next.config.ts) verifiziert: Login-Seite rendert/hydratisiert sauber, keine CSP-Verstoesse in der Konsole, Header per curl bestaetigt.
70 lines
2.5 KiB
TypeScript
70 lines
2.5 KiB
TypeScript
import type { NextConfig } from "next";
|
|
|
|
// Runs when the Node server process starts inside the container, so this
|
|
// correctly reads the deployment's actual runtime API_URL (see
|
|
// src/lib/config/runtime-config.ts for why API_URL is a runtime env var
|
|
// rather than baked into the client bundle at build time) - this is server
|
|
// startup, not the browser bundle, so it isn't subject to that constraint.
|
|
function apiOrigin(): string {
|
|
try {
|
|
return new URL(process.env.API_URL ?? "http://localhost:3001").origin;
|
|
} catch {
|
|
return "";
|
|
}
|
|
}
|
|
|
|
// No CSP nonce here on purpose: nonce-based CSP requires every page to be
|
|
// dynamically rendered (Next.js CSP guide, "Static vs Dynamic Rendering"),
|
|
// which would drop this app's static optimization across the board. This
|
|
// policy still blocks the things that matter for a mostly-same-origin app -
|
|
// cross-origin exfiltration, clickjacking, base-tag/form hijacking - via
|
|
// 'unsafe-inline' only for script/style (needed for Next's own hydration
|
|
// data and Tailwind's inline styles), matching the same trade-off the
|
|
// backend's own helmet CSP already makes for style-src.
|
|
function cspHeader(): string {
|
|
return `
|
|
default-src 'self';
|
|
script-src 'self' 'unsafe-inline';
|
|
style-src 'self' 'unsafe-inline';
|
|
img-src 'self' blob: data:;
|
|
font-src 'self';
|
|
connect-src 'self' ${apiOrigin()};
|
|
object-src 'none';
|
|
base-uri 'self';
|
|
form-action 'self';
|
|
frame-ancestors 'self';
|
|
upgrade-insecure-requests;
|
|
`
|
|
.replace(/\s{2,}/g, " ")
|
|
.trim();
|
|
}
|
|
|
|
const nextConfig: NextConfig = {
|
|
output: "standalone",
|
|
async headers() {
|
|
return [
|
|
{
|
|
// Browsers are supposed to bypass HTTP cache when checking for a new
|
|
// service worker, but that's not reliably honored everywhere - an
|
|
// explicit no-cache header here is the only way to guarantee a sw.js
|
|
// fix actually reaches already-open tabs promptly after a deploy,
|
|
// instead of possibly sitting cached for hours.
|
|
source: "/sw.js",
|
|
headers: [{ key: "Cache-Control", value: "no-cache, no-store, must-revalidate" }],
|
|
},
|
|
{
|
|
source: "/:path*",
|
|
headers: [
|
|
{ key: "Content-Security-Policy", value: cspHeader() },
|
|
{ key: "X-Frame-Options", value: "SAMEORIGIN" },
|
|
{ key: "X-Content-Type-Options", value: "nosniff" },
|
|
{ key: "Referrer-Policy", value: "no-referrer" },
|
|
{ key: "Strict-Transport-Security", value: "max-age=31536000; includeSubDomains" },
|
|
],
|
|
},
|
|
];
|
|
},
|
|
};
|
|
|
|
export default nextConfig;
|