Gradio: Mocked OAuth Login Exposes Server Credentials and Uses Hardcoded Session Secret
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.
| Package | Affected versions | Patched versions |
|---|---|---|
gradioPyPI | >= 4.16.0, < 6.6.0 | 6.6.0 |
Affected products
2- Range: >=4.16.0 <6.6.0
- gradio-app/gradiov5Range: >= 4.16.0, < 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-h3h8-3v2v-rg7mghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-27167ghsaADVISORY
- github.com/gradio-app/gradio/commit/dfee0da06d0aa94b3c2684131e7898d5d5c1911eghsaWEB
- github.com/gradio-app/gradio/releases/tag/gradio@6.6.0ghsaWEB
- github.com/gradio-app/gradio/security/advisories/GHSA-h3h8-3v2v-rg7mghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.