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.
- CWE
- CWE-601
- 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_urlPayload
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 ::
