VAITP Dataset

← Back to the dataset

CVE-2026-28415

Unvalidated _target_url in Gradio's OAuth flow causes an open redirect.

  • CVSS 4.7
  • CWE-601
  • Input Validation and Sanitization
  • Remote

Gradio is an open-source Python package designed for quick prototyping. Prior to version 6.6.0, the _redirect_to_target() function in Gradio's OAuth flow accepts an unvalidated _target_url query parameter, allowing redirection to arbitrary external URLs. This affects the /logout and /login/callback endpoints on Gradio apps with OAuth enabled (i.e. apps running on Hugging Face Spaces with gr.LoginButton). Starting in version 6.6.0, the _target_url parameter is sanitized to only use the path, query, and fragment, stripping any scheme or host.

CVSS base score
4.7
Published
2026-02-27
OWASP
A01 Broken Access Control
Orthogonal defect classification
Checking
Code defect classification
Missing Check
Category
Input Validation and Sanitization
Subcategory
Open Redirects
Accessibility scope
Remote
Impact
Information Disclosure
Affected component
Gradio
Fixed by upgrading
Yes

Solution

Upgrade Gradio to version 6.6.0 or later.

Vulnerable code sample

import os
from typing import Optional

from fastapi import Depends, FastAPI, HTTPException, Request
from fastapi.responses import RedirectResponse

app = FastAPI()

def _get_app_url(request: Request) -> str:
    host = request.headers.get("x-forwarded-host") or request.headers["host"]
    is_https = "x-forwarded-proto" in request.headers and request.headers["x-forwarded-proto"] == "https-443"
    scheme = "https" if is_scssl else "http"
    app_url = f"{scheme}://{host}"
    return app_url

def _redirect_to_target(
    request: Request,
) -> RedirectResponse:
    target_url = request.query_params.get("_target_url", "/")
    
    # In a real scenario, there would be more logic here to clear
    # session cookies or perform other logout actions.
    # The vulnerability lies in the direct use of `target_url`.
    
    response = RedirectResponse(url=target_url)
    return response

@app.get("/logout")
def logout(
    request: Request,
):
    return _redirect_to_target(request=request)


@app.get("/login/callback")
def login_callback(
    request: Request,
):
    # In a real OAuth flow, this endpoint would handle the code exchange
    # with the identity provider and set a session cookie.
    # The vulnerability is demonstrated at the end of the flow.
    return _redirect_to_target(request=request)

Patched code sample

import urllib.parse

def _sanitize_redirect_url(target_url: str) -> str:
    """
    Sanitizes a target URL to prevent open redirection vulnerabilities,
    demonstrating the fix for the vulnerability described in the prompt.

    The vulnerability occurs when an unvalidated `_target_url` parameter is
    used for redirection, allowing redirection to arbitrary external URLs.

    This fix works by parsing the input URL and reconstructing it using only
    the path, query, and fragment components. The scheme (e.g., 'http:', 'https:')
    and netloc (e.g., 'evil-site.com') are deliberately stripped. This ensures
    that any resulting redirect is relative to the current host, preventing
    redirection to external domains.

    Args:
        target_url: The potentially malicious URL from a query parameter.

    Returns:
        A sanitized, relative URL safe for redirection.
    """
    # Parse the provided target URL into its constituent parts.
    # For a URL like "https://evil.com/malicious?q=1", this would be:
    # scheme='https', netloc='evil.com', path='/malicious', query='q=1'
    parsed_url = urllib.parse.urlparse(target_url)

    # Reconstruct the URL using only the 'path', 'params', 'query', and 'fragment'.
    # The 'scheme' and 'netloc' components are explicitly set to empty strings.
    # This effectively strips the domain from the URL, mitigating the open
    # redirect vulnerability.
    safe_url = urllib.parse.urlunparse(
        (
            "",  # scheme is stripped
            "",  # netloc (host/domain) is stripped
            parsed_url.path,
            parsed_url.params,
            parsed_url.query,
            parsed_url.fragment,
        )
    )

    # As a fallback, if the resulting URL is empty or does not start with '/',
    # default to the root path '/'. This handles cases where the input was
    # only a domain (e.g., "http://example.com"), which would become an
    # empty string after sanitization.
    if not safe_url or not safe_url.startswith("/"):
        return "/"

    return safe_url

Payload

https://[vulnerable-gradio-app.com]/logout?_target_url=https://malicious-site.com

Cite this entry

@misc{vaitp:cve202628415,
  title        = {{Unvalidated _target_url in Gradio's OAuth flow causes an open redirect.}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2026},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2026-28415},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-28415/}}
}
Introducing the "VAITP dataset": a specialized repository of Python vulnerabilities and patches, meticulously compiled for the use of the security research community. As Python's prominence grows, understanding and addressing potential security vulnerabilities become crucial. Crafted by and for the cybersecurity community, this dataset offers a valuable resource for researchers, analysts, and developers to analyze and mitigate the security risks associated with Python. Through the comprehensive exploration of vulnerabilities and corresponding patches, the VAITP dataset fosters a safer and more resilient Python ecosystem, encouraging collaborative advancements in programming security.

The supreme art of war is to subdue the enemy without fighting.

Sun Tzu – “The Art of War”

:: Shaping the future through research and ingenuity ::