# RCA Report: CVE-2026-49119 — Gradio FileExplorer Path Traversal

## Summary

Gradio 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.

## Impact

- **Product / component:** `gradio` Python package, specifically `gradio/components/file_explorer.py` (`FileExplorer` component).
- **Affected versions:** Gradio before 6.16.0 (vulnerable code present at commit `0d670adf41a0b510f7fd745495dce1664d38f0e5`).
- **Fixed version:** Gradio 6.16.0 (fix commit `97d541f3d5fd05b2587a69ecc94b68fe5d2d7004`).
- **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`.

## Impact Parity

- **Disclosed / claimed maximum impact:** Information disclosure / arbitrary file read via directory traversal (`info_leak`).
- **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.
- **Parity:** `full` — the reproduction demonstrates the exact info-leak impact claimed by the ticket.
- **Not demonstrated:** N/A (full impact was reached through the real API surface).

## Root Cause

In the vulnerable `FileExplorer.preprocess()` implementation:

```python
file_ = os.path.normpath(os.path.join(self.root_dir, *file))
```

`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`.

The 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.

## Reproduction Steps

1. **Run `bundle/repro/reproduction_steps.sh`.** The script is self-contained and works from any directory; it sets `PRUVA_ROOT` automatically if not provided.
2. **What the script does:**
   - Clones or reuses the Gradio repository in the durable project cache (`/data/pruva/project-cache/.../repo`).
   - Creates a Python venv in the project cache and installs Gradio as an editable package from the repo.
   - 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.
   - 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"]]]}`.
   - Verifies that the response contains the secret string `PRUVA_SECRET_12345` read from `/tmp/gradio_secret.txt`, confirming the info leak.
   - 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).
3. **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.

## Evidence

- `bundle/logs/reproduction_steps.log` — full console output from both runs.
- `bundle/logs/server_vuln.log` — Gradio server logs for the vulnerable commit.
- `bundle/logs/server_fixed.log` — Gradio server logs for the fixed commit (contains the `InvalidPathError` traceback).
- `bundle/repro/runtime_manifest.json` — structured runtime evidence (service started, healthcheck passed, target endpoint reached, proof artifacts listed).
- `bundle/repro/validation_verdict.json` — structured verdict confirming the claim.

Key excerpt from the vulnerable run:

```json
{
  "data": ["PRUVA_SECRET_12345"],
  "is_generating": false,
  "duration": 0.0002281665802001953,
  ...
}
```

Key excerpt from the fixed run:

```json
{
  "error": "",
  "visible": true
}
```

## Recommendations / Next Steps

- **Upgrade** to Gradio 6.16.0 or later, which includes the `_safe_join` validation in `FileExplorer.preprocess()`.
- **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.
- **Regression tests** should be added that exercise `preprocess()` with `..` and absolute-path payloads, asserting that `InvalidPathError` (or equivalent) is raised.
- **Defense in depth:** run the Gradio service with minimal file-system permissions and avoid placing sensitive files in locations readable by the process.

## Additional Notes

- **Idempotency:** The reproduction script was run twice consecutively and produced the same confirmed result both times.
- **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.
- **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.
