{"repro_id":"REPRO-2026-00246","version":6,"title":"Gradio FileExplorer path traversal","repro_type":"security","status":"published","severity":"high","description":"Gradio before 6.16.0 has a path traversal vulnerability in the FileExplorer component's preprocess() method. An unauthenticated attacker can supply directory traversal sequences to escape the configured root directory and read arbitrary files. Reproduce: run a Gradio app <6.16.0 with a FileExplorer component, send a request with path traversal segments, and observe that files outside the root are read.","root_cause":"# RCA Report: CVE-2026-49119 — Gradio FileExplorer Path Traversal\n\n## Summary\n\nGradio before 6.16.0 contains a path-traversal vulnerability in the `FileExplorer` component's `preprocess()` method. When a Gradio app exposes a `FileExplorer` input, an unauthenticated remote attacker can send API requests containing `..` segments as the selected file path. The vulnerable `preprocess()` implementation joins the attacker-controlled segments directly under the configured `root_dir` using `os.path.join` + `os.path.normpath`, producing a resolved path that escapes the intended root directory. The resulting path is passed to the user-defined prediction function, enabling arbitrary file read (CWE-22) from the Gradio server host.\n\n## Impact\n\n- **Product / component:** `gradio` Python package, specifically `gradio/components/file_explorer.py` (`FileExplorer` component).\n- **Affected versions:** Gradio before 6.16.0 (vulnerable code present at commit `0d670adf41a0b510f7fd745495dce1664d38f0e5`).\n- **Fixed version:** Gradio 6.16.0 (fix commit `97d541f3d5fd05b2587a69ecc94b68fe5d2d7004`).\n- **Risk / consequences:** High. Any unauthenticated user who can reach the Gradio API can read arbitrary files readable by the Gradio process (e.g., `/etc/passwd`, application secrets, source code). The attacker only needs to know or guess the relative path structure above the configured `root_dir`.\n\n## Impact Parity\n\n- **Disclosed / claimed maximum impact:** Information disclosure / arbitrary file read via directory traversal (`info_leak`).\n- **Reproduced impact from this run:** The running vulnerable Gradio app returned the contents of a file outside the configured `root_dir` (`/tmp/gradio_secret.txt`) in response to an API call with a `..` traversal payload. The fixed commit rejected the same payload and did not return the file contents.\n- **Parity:** `full` — the reproduction demonstrates the exact info-leak impact claimed by the ticket.\n- **Not demonstrated:** N/A (full impact was reached through the real API surface).\n\n## Root Cause\n\nIn the vulnerable `FileExplorer.preprocess()` implementation:\n\n```python\nfile_ = os.path.normpath(os.path.join(self.root_dir, *file))\n```\n\n`os.path.join` simply concatenates `root_dir` with the attacker-provided path segments, and `os.path.normpath` then resolves any `..` segments. Because there is no validation that the final resolved path remains inside `root_dir`, a payload such as `[\"..\", \"gradio_secret.txt\"]` resolves to `/tmp/gradio_root/../gradio_secret.txt` → `/tmp/gradio_secret.txt`, which is outside the configured root. This behavior was inconsistent with the component's `ls()` method, which already validated paths via `_safe_join`.\n\nThe fix commit `97d541f3d5fd05b2587a69ecc94b68fe5d2d7004` (PR #13437) routes both `preprocess` branches through the same `_safe_join` helper used by `ls()`. `_safe_join` raises `InvalidPathError` when the combined path is absolute or escapes `root_dir`, blocking the traversal.\n\n## Reproduction Steps\n\n1. **Run `bundle/repro/reproduction_steps.sh`.** The script is self-contained and works from any directory; it sets `PRUVA_ROOT` automatically if not provided.\n2. **What the script does:**\n   - Clones or reuses the Gradio repository in the durable project cache (`/data/pruva/project-cache/.../repo`).\n   - Creates a Python venv in the project cache and installs Gradio as an editable package from the repo.\n   - Checks out the vulnerable commit (`0d670adf41a0b510f7fd745495dce1664d38f0e5`), starts a small Gradio app with a `FileExplorer` input rooted at `/tmp/gradio_root`, and uses `_frontend=False` so the server can run without a built frontend.\n   - Waits until the `/gradio_api/info` health endpoint responds, then sends a crafted POST to `/gradio_api/run/predict/` with payload `{\"data\": [[[\"..\", \"gradio_secret.txt\"]]]}`.\n   - Verifies that the response contains the secret string `PRUVA_SECRET_12345` read from `/tmp/gradio_secret.txt`, confirming the info leak.\n   - Repeats the same test against the fixed commit (`97d541f3d5fd05b2587a69ecc94b68fe5d2d7004`) and verifies that the secret is no longer returned (the API returns a 500 error with no secret data).\n3. **Expected evidence:** The vulnerable run logs a 200 response with `{\"data\": [\"PRUVA_SECRET_12345\"]}`. The fixed run logs a 500 response with `{\"error\": \"\", \"visible\": true}` and no secret content.\n\n## Evidence\n\n- `bundle/logs/reproduction_steps.log` — full console output from both runs.\n- `bundle/logs/server_vuln.log` — Gradio server logs for the vulnerable commit.\n- `bundle/logs/server_fixed.log` — Gradio server logs for the fixed commit (contains the `InvalidPathError` traceback).\n- `bundle/repro/runtime_manifest.json` — structured runtime evidence (service started, healthcheck passed, target endpoint reached, proof artifacts listed).\n- `bundle/repro/validation_verdict.json` — structured verdict confirming the claim.\n\nKey excerpt from the vulnerable run:\n\n```json\n{\n  \"data\": [\"PRUVA_SECRET_12345\"],\n  \"is_generating\": false,\n  \"duration\": 0.0002281665802001953,\n  ...\n}\n```\n\nKey excerpt from the fixed run:\n\n```json\n{\n  \"error\": \"\",\n  \"visible\": true\n}\n```\n\n## Recommendations / Next Steps\n\n- **Upgrade** to Gradio 6.16.0 or later, which includes the `_safe_join` validation in `FileExplorer.preprocess()`.\n- **Validate all user-provided paths** against a known-safe root directory before using them for filesystem operations. Prefer library helpers such as `safe_join` over raw `os.path.join`/`os.path.normpath` for user input.\n- **Regression tests** should be added that exercise `preprocess()` with `..` and absolute-path payloads, asserting that `InvalidPathError` (or equivalent) is raised.\n- **Defense in depth:** run the Gradio service with minimal file-system permissions and avoid placing sensitive files in locations readable by the process.\n\n## Additional Notes\n\n- **Idempotency:** The reproduction script was run twice consecutively and produced the same confirmed result both times.\n- **Environment:** The script runs in a clean sandbox using only Python 3.14 and the tools available there. It installs Gradio from source into a project-cache venv, so subsequent runs reuse the build environment while still performing fresh current-run proofs.\n- **Limitations:** The reproduction uses `_frontend=False` because the checked-out source repository does not contain the built frontend assets. This is a deployment convenience and does not affect the API surface under test; the real `/gradio_api/run/predict/` endpoint is exercised and the vulnerable `FileExplorer.preprocess()` code path is reached.\n","cve_id":"CVE-2026-49119","source_url":"https://gradio.app/changelog","package":{"name":"gradio-app/gradio","ecosystem":"github","affected_versions":"< 6.16.0","fixed_version":"6.16.0"},"reproduced_at":"2026-07-06T08:35:50.360882+00:00","duration_secs":1280.0,"tool_calls":195,"handoffs":2,"total_cost_usd":2.331107179999999,"agent_costs":{"hypothesis_generator":0.010114,"judge":0.159884,"repro":1.28994519,"support":0.03425165,"vuln_variant":0.8369123399999999},"cost_breakdown":{"hypothesis_generator":{"accounts/fireworks/models/kimi-k2p7-code":0.010114},"judge":{"gpt-5.5":0.159884},"repro":{"accounts/fireworks/models/kimi-k2p7-code":1.28994519},"support":{"accounts/fireworks/models/kimi-k2p7-code":0.03425165},"vuln_variant":{"accounts/fireworks/models/kimi-k2p7-code":0.8369123399999999}},"quality":{"confidence":"high","idempotent_verified":false,"community_verifications":0},"environment":{"sandbox_image":"ghcr.io/n3mes1s/pruva-sandbox@sha256:8096b2518d6022e13d68f885c3b8ded6b4fe607098b1a1ccbfb99abc004d1dc1"},"published_at":"2026-07-06T08:36:08.212291+00:00","retracted":false,"artifacts":[{"path":"bundle/repro/reproduction_steps.sh","filename":"reproduction_steps.sh","size":347,"category":"reproduction_script"},{"path":"bundle/repro/rca_report.md","filename":"rca_report.md","size":6614,"category":"analysis"},{"path":"bundle/vuln_variant/reproduction_steps.sh","filename":"reproduction_steps.sh","size":312,"category":"reproduction_script"},{"path":"bundle/vuln_variant/rca_report.md","filename":"rca_report.md","size":7916,"category":"analysis"},{"path":"bundle/artifact_promotion_manifest.json","filename":"artifact_promotion_manifest.json","size":5106,"category":"other"},{"path":"bundle/vuln_variant/source_identity.json","filename":"source_identity.json","size":772,"category":"other"},{"path":"bundle/vuln_variant/root_cause_equivalence.json","filename":"root_cause_equivalence.json","size":1510,"category":"other"},{"path":"bundle/logs/reproduction_steps.log","filename":"reproduction_steps.log","size":2220,"category":"log"},{"path":"bundle/repro/runtime_manifest.json","filename":"runtime_manifest.json","size":645,"category":"other"},{"path":"bundle/repro/validation_verdict.json","filename":"validation_verdict.json","size":690,"category":"other"},{"path":"bundle/logs/vuln_variant/reproduction_steps.log","filename":"reproduction_steps.log","size":5715,"category":"log"},{"path":"bundle/vuln_variant/variant_manifest.json","filename":"variant_manifest.json","size":2994,"category":"other"},{"path":"bundle/vuln_variant/validation_verdict.json","filename":"validation_verdict.json","size":729,"category":"other"},{"path":"bundle/vuln_variant/patch_analysis.md","filename":"patch_analysis.md","size":4637,"category":"documentation"},{"path":"bundle/vuln_variant/runtime_manifest.json","filename":"runtime_manifest.json","size":1186,"category":"other"}]}