feat(quick-260917-gyd): Ruecksprung nach Anmeldung auf urspruenglich angeforderte Seite

- safe-next.ts: buildNextParam()/sanitizeNextPath() als reine, getestete Funktionen (Open-Redirect-Schutz)
- middleware.ts: haengt next-Parameter an beide Login-Umleitungen (fehlendes Cookie, ungueltige Signatur)
- login/page.tsx: springt nach erfolgreicher Anmeldung auf den bereinigten next-Wert

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-17 12:26:14 +02:00
parent 4778824c73
commit 4b279eac70
4 changed files with 98 additions and 3 deletions
+73
View File
@@ -0,0 +1,73 @@
import { describe, expect, it } from 'vitest';
import { buildNextParam, sanitizeNextPath } from './safe-next';
/**
* safe-next.test — Ruecksprung nach Anmeldung (quick-260917-gyd).
*
* Reine Funktionen ohne DOM-/Node-Abhaengigkeit: `buildNextParam` baut den
* `next`-Wert aus Pfad + Query, `sanitizeNextPath` prueft ihn beim Absenden
* der Anmeldeseite gegen Open-Redirect-Versuche.
*/
describe('buildNextParam', () => {
it('Test 1: Pfad ohne Query bleibt unveraendert', () => {
expect(buildNextParam('/settings/general/desktop', '')).toBe(
'/settings/general/desktop',
);
});
it('Test 2: _rsc wird entfernt, andere Parameter bleiben', () => {
expect(buildNextParam('/modules/tender-radar', '?tab=alerts&_rsc=1abc')).toBe(
'/modules/tender-radar?tab=alerts',
);
});
it('Test 3: leere Query nach Entfernen von _rsc bleibt ohne "?"', () => {
expect(buildNextParam('/modules/tender-radar', '?_rsc=1abc')).toBe(
'/modules/tender-radar',
);
});
it('Test 4: Startseite und Anmeldeseite liefern null (kein Ruecksprung auf sich selbst)', () => {
expect(buildNextParam('/', '')).toBeNull();
expect(buildNextParam('/login', '?next=%2Fx')).toBeNull();
expect(buildNextParam('/login/', '')).toBeNull();
});
});
describe('sanitizeNextPath', () => {
it('Test 5: gueltige relative Pfade bleiben unveraendert', () => {
expect(sanitizeNextPath('/settings/general/desktop')).toBe(
'/settings/general/desktop',
);
expect(sanitizeNextPath('/modules/x?tab=1')).toBe('/modules/x?tab=1');
});
it('Test 6: fehlende oder falsch typisierte Werte fallen auf "/" zurueck', () => {
expect(sanitizeNextPath(null)).toBe('/');
expect(sanitizeNextPath(undefined)).toBe('/');
expect(sanitizeNextPath('')).toBe('/');
expect(sanitizeNextPath(42)).toBe('/');
});
it('Test 7: protokoll-relative und Schema-Adressen fallen auf "/" zurueck', () => {
expect(sanitizeNextPath('//evil.example')).toBe('/');
expect(sanitizeNextPath('/\\evil.example')).toBe('/');
expect(sanitizeNextPath('https://evil.example/x')).toBe('/');
expect(sanitizeNextPath('javascript:alert(1)')).toBe('/');
});
it('Test 8: fehlender Slash, Steuerzeichen und Ueberlaenge fallen auf "/" zurueck', () => {
expect(sanitizeNextPath('settings')).toBe('/');
expect(sanitizeNextPath('/foo\nbar')).toBe('/');
expect(sanitizeNextPath('/a b')).toBe('/');
expect(sanitizeNextPath('/x'.padEnd(3000, 'y'))).toBe('/');
});
it('Test 9: die Anmeldeseite selbst ist kein gueltiges Ziel, "/loginhistory" schon', () => {
expect(sanitizeNextPath('/login')).toBe('/');
expect(sanitizeNextPath('/login?next=/x')).toBe('/');
expect(sanitizeNextPath('/login/')).toBe('/');
expect(sanitizeNextPath('/loginhistory')).toBe('/loginhistory');
});
});
Binary file not shown.