fix(login): close ?next= control-char open-redirect + cover register
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
49
frontend/src/lib/safeNext.test.ts
Normal file
49
frontend/src/lib/safeNext.test.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { safeNext } from './safeNext';
|
||||
|
||||
describe('safeNext', () => {
|
||||
it('allows same-origin absolute paths', () => {
|
||||
expect(safeNext('/library')).toBe('/library');
|
||||
expect(safeNext('/search?q=berserk')).toBe('/search?q=berserk');
|
||||
expect(safeNext('/manga/123#page-2')).toBe('/manga/123#page-2');
|
||||
});
|
||||
|
||||
it('falls back to / for missing or empty input', () => {
|
||||
expect(safeNext(null)).toBe('/');
|
||||
expect(safeNext(undefined)).toBe('/');
|
||||
expect(safeNext('')).toBe('/');
|
||||
});
|
||||
|
||||
it('rejects protocol-relative URLs (open redirect)', () => {
|
||||
expect(safeNext('//evil.com')).toBe('/');
|
||||
expect(safeNext('//evil.com/path')).toBe('/');
|
||||
});
|
||||
|
||||
it('rejects backslash tricks browsers normalise to //', () => {
|
||||
expect(safeNext('/\\evil.com')).toBe('/');
|
||||
expect(safeNext('/\\/evil.com')).toBe('/');
|
||||
});
|
||||
|
||||
it('rejects control/whitespace chars browsers strip mid-parse', () => {
|
||||
// These start with a single "/" so the prefix checks alone would pass
|
||||
// them, but a browser strips the tab/newline/CR and the value
|
||||
// collapses to a protocol-relative //evil.com — the %09/%0A/%0D
|
||||
// open-redirect bypass.
|
||||
expect(safeNext('/\t/evil.com')).toBe('/'); // %09 tab
|
||||
expect(safeNext('/\n/evil.com')).toBe('/'); // %0A newline
|
||||
expect(safeNext('/\r/evil.com')).toBe('/'); // %0D carriage return
|
||||
expect(safeNext('/ /evil.com')).toBe('/'); // space
|
||||
expect(safeNext(`/${String.fromCharCode(0)}/evil.com`)).toBe('/'); // NUL
|
||||
expect(safeNext(`/${String.fromCharCode(0x2028)}/evil.com`)).toBe('/'); // line sep
|
||||
});
|
||||
|
||||
it('rejects absolute and scheme URLs', () => {
|
||||
expect(safeNext('https://evil.com')).toBe('/');
|
||||
expect(safeNext('http://evil.com')).toBe('/');
|
||||
expect(safeNext('javascript:alert(1)')).toBe('/');
|
||||
});
|
||||
|
||||
it('rejects bare paths without a leading slash', () => {
|
||||
expect(safeNext('library')).toBe('/');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user