VYPR
Moderate severityNVD Advisory· Published Feb 27, 2026· Updated Mar 2, 2026

Gradio has Open Redirect in OAuth Flow

CVE-2026-28415

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.

PackageAffected versionsPatched versions
gradioPyPI
< 6.6.06.6.0

Affected products

2

Patches

1
dfee0da06d0a

Oauth fixes (#12884)

https://github.com/gradio-app/gradioFreddy BoultonFeb 17, 2026via ghsa
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

News mentions

0

No linked articles in our index yet.