Fix open redirect vulnerability in LoginPage

Validate the ?next= query parameter to prevent open redirects to
external URLs. The parameter is validated to ensure it is a relative
path (starts with / but not //) before using it for navigation.
Invalid paths fall back to '/'.

This prevents attackers from crafting login links like /login?next=https://evil.com
that would transparently redirect authenticated users to malicious sites.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-04-22 21:04:17 +02:00
parent a286ede49c
commit 0481810226
2 changed files with 9 additions and 2 deletions

View File

@@ -4,6 +4,12 @@ This document catalogues architecture violations, code smells, and structural is
---
## Security Fixes
- Fixed open redirect vulnerability in `frontend/src/pages/LoginPage.tsx` by validating the `?next=` parameter to ensure it is a relative path (starts with `/` but not `//`). The validation regex `/^\/(?!\/)/.test(next)` prevents protocol-relative URLs and external redirects. Invalid paths fall back to `"/"`.
---
## Completed Refactors
- Moved `Fail2BanConnectionError` and `Fail2BanProtocolError` from `backend/app/utils/fail2ban_client.py` into `backend/app/exceptions.py`. Updated all router, service, and test call sites to import these domain exceptions from `app.exceptions` and retained backward compatibility through re-exporting in `app.utils.fail2ban_client`.

View File

@@ -81,7 +81,8 @@ export function LoginPage(): React.JSX.Element {
const [error, setError] = useState<string | null>(null);
const [submitting, setSubmitting] = useState(false);
const nextPath = searchParams.get("next") ?? "/";
const next = searchParams.get("next") ?? "";
const safePath = /^\/(?!\/)/.test(next) ? next : "/";
function handlePasswordChange(ev: ChangeEvent<HTMLInputElement>): void {
setPassword(ev.target.value);
@@ -100,7 +101,7 @@ export function LoginPage(): React.JSX.Element {
try {
await login(password);
navigate(nextPath, { replace: true });
navigate(safePath, { replace: true });
} catch (err) {
if (err instanceof ApiError && err.status === 401) {
setError("Incorrect password. Please try again.");