Skip to content

[Security] Open Redirect in UserLoginFormAdmin and UserRegFormAdmin via unvalidated redirect query param #54

Description

@sudu787

[SECURITY ADVISORY] Open Redirect in Login and Registration Flows via Unvalidated redirect Parameter

Title

Open Redirect Vulnerability via Unvalidated redirect Query Parameter in UserLoginFormAdmin and UserRegFormAdmin

Summary

An Open Redirect vulnerability (CWE-601) exists in fastapi-user-auth in the authentication and registration form handling logic.

The application retrieves the redirect query parameter from the HTTP request and utilizes it directly in server-side HTTP 307 Temporary Redirect responses and client-side form redirect attributes without validating the destination host or URL scheme.

An attacker can construct a crafted login or registration URL containing an arbitrary external target (e.g., https://evil.com), enticing users to authenticate and subsequently redirecting them to a malicious phishing site.


Vulnerability Details

  • Vulnerability Type: URL Redirection to Untrusted Site ('Open Redirect')

  • CWE ID: [CWE-601](https://cwe.mitre.org/data/definitions/601.html)

  • Severity: Medium (CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N — Score: 6.1)

  • Affected Components:

    • fastapi_user_auth/admin/admin.py (UserLoginFormAdmin.get_form, UserLoginFormAdmin.route_page, UserRegFormAdmin.get_form)

Root Cause Analysis

In fastapi_user_auth/admin/admin.py, the redirect query parameter is read without any hostname or path validation:

1. Server-Side 307 Redirect (lines 127–134)

When an authenticated user visits the login page, the server issues an immediate HTTP 307 redirect using the unvalidated location header:

# fastapi_user_auth/admin/admin.py:127-134
@property
def route_page(self) -> Callable:
    async def route(request: Request, result=Depends(super().route_page)):
        if request.user:
            raise HTTPException(
                status_code=status.HTTP_307_TEMPORARY_REDIRECT,
                detail="already logged in",
                headers={"location": request.query_params.get("redirect") or "/"},
            )
        return result

    return route

2. Client-Side Form Redirect (lines 118 & 190)

In both UserLoginFormAdmin.get_form and UserRegFormAdmin.get_form, the form redirect target is populated directly from user input:

# Line 118 (UserLoginFormAdmin) & Line 190 (UserRegFormAdmin)
form.redirect = request.query_params.get("redirect") or "/"

Because there is no check ensuring that the destination is relative (e.g., /dashboard) or restricted to a trusted whitelist of domains, external URLs such as https://evil.com or dangerous URI schemes (e.g., javascript:) are accepted.


Steps to Reproduce (Proof of Concept)

Scenario A: Server-Side 307 Redirection (Logged-in User)

  1. Log in to the application.
  2. Visit the following crafted URL:
GET /auth/login?redirect=https://evil.com HTTP/1.1
Host: target-app.com
  1. Observed Response

The browser immediately navigates to evil.com.

Scenario B: Post-Authentication Redirection (Unauthenticated User)

Send a victim a phishing link pointing to the legitimate site:

https://target-app.com/auth/login?redirect=https://evil.com/fake-login

The user sees a valid domain (target-app.com) and enters their legitimate credentials.

Upon successful submission, the client-side UI executes form.redirect and sends the user to:

https://evil.com/fake-login

Impact

Credential Harvesting / Phishing

Attackers can construct convincing phishing campaigns that leverage the legitimate domain's reputation to redirect victims to malicious sites.

Session / Token Exposure

If combined with browser quirks or token parameters, sensitive authentication context may be leaked in HTTP Referer headers.


Remediation / Suggested Patch

Implement URL validation to restrict redirects strictly to relative paths (same-origin):

--- a/fastapi_user_auth/admin/admin.py
+++ b/fastapi_user_auth/admin/admin.py
@@ -1,4 +1,5 @@
 import contextlib
+from urllib.parse import urlparse
 from typing import Any, Callable, Dict, List, Type
 
 from fastapi import Depends, HTTPException
@@ -63,6 +64,15 @@ from fastapi_user_auth.mixins.admin import AuthFieldModelAdmin, AuthSelectModelA
 
+def _get_safe_redirect(url: str, default: str = "/") -> str:
+    """Validate redirect URL to ensure it is relative and does not redirect externally."""
+    if not url:
+        return default
+    parsed = urlparse(url)
+    if parsed.scheme or parsed.netloc:
+        return default
+    return url
+
 class UserLoginFormAdmin(FormAdmin):
@@ -118,1 +128,1 @@ class UserLoginFormAdmin(FormAdmin):
-        form.redirect = request.query_params.get("redirect") or "/"
+        form.redirect = _get_safe_redirect(request.query_params.get("redirect"))
@@ -132,1 +142,1 @@ class UserLoginFormAdmin.route_page:
-                    headers={"location": request.query_params.get("redirect") or "/"},
+                    headers={"location": _get_safe_redirect(request.query_params.get("redirect"))},
@@ -190,1 +200,1 @@ class UserRegFormAdmin(UserRegFormAdmin):
-        form.redirect = request.query_params.get("redirect") or "/"
+        form.redirect = _get_safe_redirect(request.query_params.get("redirect"))

---

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions