Gradio has Open Redirect in OAuth Flow
Description
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.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Gradio before 6.6.0 has an open redirect vulnerability in its OAuth flow; the _redirect_to_target() function does not validate the _target_url parameter, allowing redirection to arbitrary external URLs via /logout and /login/callback endpoints.
Vulnerability
Overview
CVE-2026-28415 describes an open redirect vulnerability in Gradio, an open-source Python package for building machine learning demos and web applications. 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 that have OAuth enabled, such as those running on Hugging Face Spaces with gr.LoginButton [2].
Exploitation
Conditions
The vulnerability is exploitable by an attacker who can craft a URL containing a malicious _target_url parameter. When a user visits the manipulated /logout or /login/callback endpoint, the application will redirect the user's browser to the attacker-controlled external URL without any validation. No authentication is required to trigger the redirect; the attacker simply needs to lure a victim to click on the crafted link [2][3].
Impact
A successful open redirect can be leveraged in phishing campaigns, where a victim trusts the legitimate Gradio domain but is silently redirected to a malicious site that may steal credentials or deliver malware. Additionally, this can undermine the integrity of OAuth flows, potentially leading to the leakage of authorization codes or tokens if the redirect is exploited in a more complex attack chain [2].
Mitigation
The vulnerability is fixed in Gradio version 6.6.0. The fix sanitizes the _target_url parameter by stripping the scheme and host, only retaining the path, query, and fragment. This is evident in the commit that modified the _redirect_to_target() function to use urllib.parse.urlparse() to extract only the safe components [3]. Users should upgrade to version 6.6.0 or later. The release notes for version 6.6.0 confirm this fix as part of the "Oauth fixes" update [4].
AI Insight generated on May 18, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
gradioPyPI | < 6.6.0 | 6.6.0 |
Affected products
2- Range: <6.6.0
- gradio-app/gradiov5Range: < 6.6.0
Patches
1dfee0da06d0aOauth fixes (#12884)
3 files changed · +73 −2
.changeset/neat-hairs-share.md+5 −0 added@@ -0,0 +1,5 @@ +--- +"gradio": patch +--- + +fix:Oauth fixes
gradio/oauth.py+9 −2 modified@@ -220,7 +220,14 @@ def _redirect_to_target( request: fastapi.Request, default_target: str = "/" ) -> RedirectResponse: target = request.query_params.get("_target_url", default_target) - return RedirectResponse(target) + # Prevent open redirect by stripping scheme/host and only using the path. + parsed = urllib.parse.urlparse(target) + safe_target = parsed.path or "/" + if parsed.query: + safe_target += "?" + parsed.query + if parsed.fragment: + safe_target += "#" + parsed.fragment + return RedirectResponse(safe_target) @dataclass @@ -323,7 +330,7 @@ def _get_mocked_oauth_info() -> typing.Dict: ) return { - "access_token": token, + "access_token": "mock-oauth-token-for-local-dev", "token_type": "bearer", "expires_in": 3600, "id_token": "AAAAAAAAAAAAAAAAAAAAAAAAAA",
test/test_routes.py+59 −0 modified@@ -2053,3 +2053,62 @@ def test_json_postprocessing_with_queue_false(connect): with connect(demo) as client: output = client.predict(api_name="/lambda") assert output == {"epochs": 20, "learning_rate": 0.001, "batch_size": 32} + + +class TestOAuthSecurity: + def test_redirect_to_target_blocks_external_urls(self): + """_redirect_to_target should strip scheme/host to prevent open redirects.""" + from gradio.oauth import _redirect_to_target + + scope = { + "type": "http", + "method": "GET", + "headers": [], + } + + # External URL should be stripped to just the path + scope["query_string"] = b"_target_url=https://evil.com/steal" + request = Request(scope) + response = _redirect_to_target(request) + assert response.headers["location"] == "/steal" + + # Protocol-relative URL should be stripped + scope["query_string"] = b"_target_url=//evil.com/steal" + request = Request(scope) + response = _redirect_to_target(request) + assert response.headers["location"] == "/steal" + + # Relative path should pass through unchanged + scope["query_string"] = b"_target_url=/my-page%3Ffoo%3Dbar" + request = Request(scope) + response = _redirect_to_target(request) + location = response.headers["location"] + assert location == "/my-page?foo=bar" + + # Default target when no _target_url + scope["query_string"] = b"" + request = Request(scope) + response = _redirect_to_target(request) + assert response.headers["location"] == "/" + + def test_mocked_oauth_does_not_leak_real_token(self): + """_get_mocked_oauth_info should return a dummy token, not the real HF token.""" + from unittest.mock import patch + + from gradio.oauth import _get_mocked_oauth_info + + with ( + patch("gradio.oauth.get_token", return_value="hf_real_secret_token"), + patch( + "gradio.oauth.whoami", + return_value={ + "type": "user", + "fullname": "Test User", + "name": "testuser", + "avatarUrl": "https://huggingface.co/avatar.png", + }, + ), + ): + info = _get_mocked_oauth_info() + assert info["access_token"] != "hf_real_secret_token" + assert info["access_token"] == "mock-oauth-token-for-local-dev"
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-pfjf-5gxr-995xghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-28415ghsaADVISORY
- github.com/gradio-app/gradio/commit/dfee0da06d0aa94b3c2684131e7898d5d5c1911eghsaWEB
- github.com/gradio-app/gradio/releases/tag/gradio%406.6.0ghsaWEB
- github.com/gradio-app/gradio/security/advisories/GHSA-pfjf-5gxr-995xghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.