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:
MechaCat02
2026-07-01 07:18:01 +02:00
parent 4306d1c96a
commit 3ba30f4ba9
8 changed files with 142 additions and 5 deletions

View File

@@ -81,6 +81,52 @@ test('login then logout flips the layout between authenticated and anonymous', a
await expect(page.getByTestId('nav-login')).toBeVisible();
});
test('login redirects to a safe ?next= target after authenticating', async ({ page }) => {
await stubAnonymousThenAuthenticated(page);
// Arrive at /login carrying a same-origin ?next= (the shape the layout
// produces when bouncing an unauthenticated user off a gated page).
await page.goto('/login?next=%2Fsearch');
await expect(page.getByTestId('nav-login')).toBeVisible();
await page.getByTestId('login-username').fill('alice');
await page.getByTestId('login-password').fill('hunter2hunter2');
await page.getByTestId('login-submit').click();
// Lands on the requested page, not the default root.
await expect(page).toHaveURL(/\/search$/);
});
test('login ignores an unsafe ?next= and falls back to /', async ({ page }) => {
await stubAnonymousThenAuthenticated(page);
// A protocol-relative open-redirect attempt must be discarded.
await page.goto('/login?next=%2F%2Fevil.com');
await expect(page.getByTestId('nav-login')).toBeVisible();
await page.getByTestId('login-username').fill('alice');
await page.getByTestId('login-password').fill('hunter2hunter2');
await page.getByTestId('login-submit').click();
// Authenticated and on the site root — never navigated off-origin.
await expect(page).toHaveURL(/\/$/);
await expect(page.getByTestId('session-user')).toContainText('alice');
});
test('login rejects the %09 control-char open-redirect bypass', async ({ page }) => {
await stubAnonymousThenAuthenticated(page);
// "/\t/evil.com" passes a naive startsWith('/') && !startsWith('//')
// guard but collapses to "//evil.com" once the browser strips the tab.
await page.goto('/login?next=%2F%09%2Fevil.com');
await expect(page.getByTestId('nav-login')).toBeVisible();
await page.getByTestId('login-username').fill('alice');
await page.getByTestId('login-password').fill('hunter2hunter2');
await page.getByTestId('login-submit').click();
// Stayed on-origin at the root, authenticated.
await expect(page).toHaveURL(/\/$/);
await expect(page.getByTestId('session-user')).toContainText('alice');
});
test('login surfaces the API error message on bad credentials', async ({ page }) => {
await page.route('**/api/v1/auth/me', async (route) => {
await route.fulfill({