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

Gradio: Mocked OAuth Login Exposes Server Credentials and Uses Hardcoded Session Secret

CVE-2026-27167

Description

Gradio is an open-source Python package designed for quick prototyping. Starting in version 4.16.0 and prior to version 6.6.0, Gradio applications running outside of Hugging Face Spaces automatically enable "mocked" OAuth routes when OAuth components (e.g. gr.LoginButton) are used. When a user visits /login/huggingface, the server retrieves its own Hugging Face access token via huggingface_hub.get_token() and stores it in the visitor's session cookie. If the application is network-accessible, any remote attacker can trigger this flow to steal the server owner's HF token. The session cookie is signed with a hardcoded secret derived from the string "-v4", making the payload trivially decodable. Version 6.6.0 fixes the issue.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Gradio versions 4.16.0 to 6.5.2 expose the server's Hugging Face token to any visitor via a mocked OAuth route and a hardcoded session cookie secret.

Vulnerability

Overview CVE-2026-27167 affects Gradio, an open-source Python library for building machine learning demos. Starting in version 4.16.0 and prior to 6.6.0, when OAuth components like gr.LoginButton are used in applications not running on Hugging Face Spaces, the framework automatically enables "mocked" OAuth routes [2][3]. This means a visiting user hitting /login/huggingface triggers the server to fetch its own Hugging Face access token via huggingface_hub.get_token() and stores that token in the visitor's session cookie [3]. The session cookie is signed using a hardcoded secret derived from the string `"-v4", making the signature trivial to forge or ignore, exposing the token payload to anyone who inspects the cookie [2][3].

Attack

Vector An attacker who can make a single HTTP request to a vulnerable Gradio instance can exploit this without any prior authentication [2][3]. The victim is the server operator: when a remote user navigates to /login/huggingface, the server injects its own Hugging Face access token into that user's session [3]. Because the signing secret is static and public, the attacker can decode the cookie contents at will, gaining possession of the token [2][3]. The attack surface includes any network-accessible Gradio server that includes an OAuth component.

Impact

Successful exploitation yields the server owner's Hugging Face access token [2][3]. With this token, an attacker could impersonate the server operator on Hugging Face, potentially accessing private repositories, modifying models, or performing actions on the user's behalf. The GitHub Security Advisory also notes that cookie forgery could allow an attacker to craft arbitrary session data [3].

Mitigation

The vulnerability is fixed in Gradio version 6.6.0 [2]. The fix replaces the real token with a placeholder string "mock-oauth-token-for-local-dev" in mocked OAuth routes and updates the session secret to not rely on a hardcoded constant [4]. Users are advised to upgrade immediately.

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
>= 4.16.0, < 6.6.06.6.0

Affected products

2
  • Range: >=4.16.0 <6.6.0
  • gradio-app/gradiov5
    Range: >= 4.16.0, < 6.6.0

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.