/** * safe-next — Ruecksprung nach Anmeldung (quick-260917-gyd). * * Reine Funktionen ohne Node- oder DOM-APIs: die Datei wird sowohl von der * Edge-Middleware (middleware.ts) als auch vom Client (login/page.tsx, * header.tsx) importiert. * * `sanitizeNextPath()` ist der Open-Redirect-Schutz (T-gyd-01): sie laesst * ausschliesslich sichere relative Pfade durch. Schema-Adressen wie * `https://…` oder `javascript:…` scheitern bereits an der Regel "erstes * Zeichen muss `/` sein" — eine zusaetzliche Liste bekannter Schemata ist * deshalb nicht noetig und wird bewusst nicht gepflegt. */ const MAX_NEXT_PATH_LENGTH = 2048; // Backslash, jegliches Whitespace sowie die Steuerzeichen 0-31 und 127 // (als Unicode-Escapes, nicht \x, weil safe-next.ts auch am Edge laeuft). const FORBIDDEN_CHARS_RE = /[\\\s-]/; /** * Erzeugt den `next`-Wert aus Pfad und Query fuer die Login-Umleitung. * Entfernt `_rsc` (Next.js haengt den Parameter an RSC-Navigationsanfragen * an, er hat in der Login-URL nichts verloren). Liefert `null` fuer `/` * und fuer die Anmeldeseite selbst (`/login`, `/login/...`) — dorthin gibt * es keinen sinnvollen Ruecksprung. */ export function buildNextParam( pathname: string, search: string, ): string | null { if ( pathname === '/' || pathname === '/login' || pathname.startsWith('/login/') ) { return null; } const params = new URLSearchParams(search); params.delete('_rsc'); const query = params.toString(); return query ? `${pathname}?${query}` : pathname; } /** * Prueft einen aus der URL gelesenen `next`-Wert. Liefert ihn unveraendert * zurueck, wenn er ein sicherer relativer Pfad ist, sonst `'/'`. */ export function sanitizeNextPath(raw: unknown): string { if ( typeof raw !== 'string' || raw.length === 0 || raw.length > MAX_NEXT_PATH_LENGTH ) { return '/'; } // Erstes Zeichen muss `/` sein (schliesst Schema-Adressen wie // `https://…`/`javascript:…` automatisch aus); zweites Zeichen darf // weder `/` noch `\` sein (protokoll-relative Adressen wie `//host` und // die Backslash-Variante, die Browser ebenfalls als Slash lesen). if (raw[0] !== '/' || raw[1] === '/' || raw[1] === '\\') { return '/'; } if (FORBIDDEN_CHARS_RE.test(raw)) { return '/'; } const pathPart = raw.split(/[?#]/, 1)[0]; if (pathPart === '/login' || pathPart.startsWith('/login/')) { return '/'; } return raw; }