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"))

---

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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