From c95e1c4b1824cefb91c1f6ec3b81b12ce52da0d9 Mon Sep 17 00:00:00 2001 From: Nick Szaunig Date: Wed, 2 Sep 2026 21:44:23 +0200 Subject: [PATCH] fix: Login komplett kaputt seit dem Security-Header-Commit - CSP blockierte jede API-Anfrage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root Cause, zweifach: 1. proxy.ts lag im Projekt-Root statt unter src/ (wo app/ tatsaechlich liegt). Next.js hat die Datei dadurch nie erkannt - ueberprueft via Build-Output ("Proxy (Middleware)" fehlte komplett) und via routes-manifest.json. Das betraf nicht nur die neuen Security-Header, sondern auch die schon laenger bestehende Login-Redirect-Logik, die dadurch vermutlich nie serverseitig gegriffen hat - vorher unbemerkt, weil Client-seitige Auth-Checks das kaschiert haben. 2. Die CSP (mit dynamischem connect-src aus API_URL) lag in next.config.ts's headers() - die wird beim Docker-Build EINMAL aufgeloest und ins Standalone-Output eingefroren, nicht bei jedem Container-Start neu gelesen, wie im Code-Kommentar faelschlich behauptet. Jedes Deployment bekam dadurch permanent "connect-src http://localhost:3001" (den Fallback, da API_URL beim Build nicht gesetzt ist) - unabhaengig vom tatsaechlich im Pod gesetzten API_URL. Jede Anfrage vom Frontend ans Backend (Login, SSO-Status, Herkunftsseite) wurde dadurch von jedem Browser CSP-blockiert. Fix: CSP + Security-Header ziehen nach proxy.ts (jetzt korrekt unter src/) um - Proxy laeuft mit Node.js-Runtime frisch bei jeder Anfrage, liest process.env.API_URL also korrekt zur Laufzeit. next.config.ts behaelt nur noch die statische sw.js-Cache-Control-Regel. Lokal end-to-end verifiziert: Build zeigt jetzt "ƒ Proxy (Middleware)" (vorher komplett abwesend), CSP-Header zeigt nach Server-Start einen distinct getesteten API_URL-Wert korrekt an (nicht mehr den Localhost-Fallback), und ein echter Login-Klick im Browser schickt die Anfrage jetzt an die richtige, zur Laufzeit gesetzte Backend-Origin. Co-Authored-By: Claude Sonnet 5 --- ApiCore-Frontend/next.config.ts | 58 ++++---------------- ApiCore-Frontend/proxy.ts | 32 ----------- ApiCore-Frontend/src/proxy.ts | 96 +++++++++++++++++++++++++++++++++ 3 files changed, 105 insertions(+), 81 deletions(-) delete mode 100644 ApiCore-Frontend/proxy.ts create mode 100644 ApiCore-Frontend/src/proxy.ts diff --git a/ApiCore-Frontend/next.config.ts b/ApiCore-Frontend/next.config.ts index 81d405e..9726c18 100644 --- a/ApiCore-Frontend/next.config.ts +++ b/ApiCore-Frontend/next.config.ts @@ -1,44 +1,14 @@ 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(); -} - +// Security headers (CSP, X-Frame-Options, etc.) live in proxy.ts, not here. +// This file's headers() is resolved once at build time and frozen into the +// standalone output - fine for a static value like the sw.js rule below, but +// wrong for the CSP's connect-src, which needs the container's actual +// runtime API_URL. That mistake shipped a permanently-wrong connect-src +// (build-time env, if any, or the localhost fallback) to every environment +// regardless of the real deployed API_URL, silently CSP-blocking every API +// call including login. Proxy runs fresh on every request and reads env +// correctly - see proxy.ts for the real header logic. const nextConfig: NextConfig = { output: "standalone", async headers() { @@ -52,16 +22,6 @@ const nextConfig: NextConfig = { 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" }, - ], - }, ]; }, }; diff --git a/ApiCore-Frontend/proxy.ts b/ApiCore-Frontend/proxy.ts deleted file mode 100644 index 8f2b2cb..0000000 --- a/ApiCore-Frontend/proxy.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { NextResponse } from "next/server"; -import type { NextRequest } from "next/server"; - -const SESSION_COOKIE = "refreshToken"; - -export function proxy(request: NextRequest) { - const hasSession = request.cookies.has(SESSION_COOKIE); - const { pathname } = request.nextUrl; - - if (pathname.startsWith("/login")) { - if (hasSession) { - return NextResponse.redirect(new URL("/settings/users", request.url)); - } - return NextResponse.next(); - } - - if (!hasSession) { - return NextResponse.redirect(new URL("/login", request.url)); - } - - return NextResponse.next(); -} - -export const config = { - matcher: [ - "/login", - "/settings/:path*", - "/apiaries/:path*", - "/colonies/:path*", - "/queens/:path*", - ], -}; diff --git a/ApiCore-Frontend/src/proxy.ts b/ApiCore-Frontend/src/proxy.ts new file mode 100644 index 0000000..c6f4c24 --- /dev/null +++ b/ApiCore-Frontend/src/proxy.ts @@ -0,0 +1,96 @@ +import { NextResponse } from "next/server"; +import type { NextRequest } from "next/server"; + +const SESSION_COOKIE = "refreshToken"; + +// Paths that redirect to /login when there's no session - kept identical to +// the set this file already covered before security headers moved in here. +// Broadening this would silently change auth-redirect behaviour for pages +// that currently rely on client-side auth checks instead (dashboard, +// bottling, etc.) and must not now start doing it as a side effect of the +// matcher below being widened for headers. +const AUTH_REDIRECT_PATTERNS = [ + /^\/settings(\/.*)?$/, + /^\/apiaries(\/.*)?$/, + /^\/colonies(\/.*)?$/, + /^\/queens(\/.*)?$/, +]; + +function needsAuthRedirectCheck(pathname: string): boolean { + return AUTH_REDIRECT_PATTERNS.some((pattern) => pattern.test(pathname)); +} + +// Deliberately computed here, not in next.config.ts: that file's headers() +// is resolved once at build time and frozen into the standalone output, so +// process.env.API_URL there is whatever was (or wasn't) set during the +// Docker build - never the container's actual runtime value. Proxy runs the +// Node.js runtime fresh on every request, so this reads the real deployed +// API_URL correctly instead of silently falling back to localhost forever. +function apiOrigin(): string { + try { + return new URL(process.env.API_URL ?? "http://localhost:3001").origin; + } catch { + return ""; + } +} + +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(); +} + +function withSecurityHeaders(response: NextResponse): NextResponse { + response.headers.set("Content-Security-Policy", cspHeader()); + response.headers.set("X-Frame-Options", "SAMEORIGIN"); + response.headers.set("X-Content-Type-Options", "nosniff"); + response.headers.set("Referrer-Policy", "no-referrer"); + response.headers.set( + "Strict-Transport-Security", + "max-age=31536000; includeSubDomains", + ); + return response; +} + +export function proxy(request: NextRequest) { + const hasSession = request.cookies.has(SESSION_COOKIE); + const { pathname } = request.nextUrl; + + if (pathname.startsWith("/login")) { + if (hasSession) { + return withSecurityHeaders( + NextResponse.redirect(new URL("/settings/users", request.url)), + ); + } + return withSecurityHeaders(NextResponse.next()); + } + + if (needsAuthRedirectCheck(pathname) && !hasSession) { + return withSecurityHeaders( + NextResponse.redirect(new URL("/login", request.url)), + ); + } + + return withSecurityHeaders(NextResponse.next()); +} + +export const config = { + // Broadened from the original 5-path list so every page gets the security + // headers (not just the ones that also happen to need the auth-redirect + // check, which is now handled separately above, unchanged in scope). + matcher: [ + "/((?!_next/static|_next/image|favicon.ico|sw.js|manifest.webmanifest|icon.svg).*)", + ], +};