f85c91be01
FORBIDDEN_CHARS_RE in sanitizeNextPath bestand aus drei rohen Steuerbytes (NUL, US, DEL) statt Escapes - jeder Editor, Formatierer oder Minifier in der Kette kann solche Bytes stillschweigend verschlucken. Umgeschrieben auf Unicode-Escapes fuer den Bereich U+0000 bis U+001F und U+007F. Zeichenmenge ueber alle 65536 Codepunkte aus U+0000 bis U+FFFF als unveraendert nachgewiesen (54 abgewiesene Codepunkte, Bitmap-SHA-256 3d58108b87e4641e506602cc701a66d11ec19cf20551937fada1826f50ecbe64 vor und nach dem Umbau identisch). Kommentar korrigiert: die vorherige Behauptung, Escapes seien "am Edge" noetig, war falsch - zwischen den beiden Escape-Schreibweisen gibt es zur Laufzeit keinen Unterschied. Quick-Vorgang 260921-i8x, Task 1/3. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
80 lines
2.7 KiB
TypeScript
80 lines
2.7 KiB
TypeScript
/**
|
|
* 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, die Steuerzeichen U+0000 bis U+001F
|
|
// sowie U+007F. Die Datei wird von der Edge-Middleware und vom Client
|
|
// importiert und darf deshalb weder Node- noch DOM-APIs verwenden.
|
|
// biome-ignore lint/suspicious/noControlCharactersInRegex: Die Regel beanstandet diesen Bereich auch als Unicode-Escape; genau dieser Bereich ist der Zweck der Zeichenklasse, sie traegt den Weiterleitungsschutz T-gyd-01.
|
|
const FORBIDDEN_CHARS_RE = /[\\\s\u0000-\u001f\u007f]/;
|
|
|
|
/**
|
|
* 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;
|
|
}
|