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:
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user