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

## Summary

This variant stage searched for additional triggers and bypasses of the Gradio `FileExplorer` path traversal (CVE-2026-49119). Two alternate attacker-controlled payloads were confirmed to reach the same vulnerable sink in `gradio/components/file_explorer.py:FileExplorer.preprocess()` on the unpatched commit (`0d670adf`):

1. **Absolute path payload:** `[[["/tmp/gradio_secret.txt"]]]` — the `os.path.join(self.root_dir, *file)` call discards the configured `root_dir` when the first segment is absolute and returns an arbitrary absolute path.
2. **Deep relative traversal payload:** `[[["..", "..", "tmp", "gradio_secret.txt"]]]` — multiple `..` segments normalize to a path outside `root_dir`.

Both payloads leak the target file on the vulnerable commit, but the patched commit (`97d541f3`, Gradio 6.16.0) rejects them via the `_safe_join`/`safe_join` validation. Therefore, these are **alternate triggers of the same root cause**, not bypasses of the fix. The latest default-branch commit (`a34843eda`) still contains the same hardened code.

## Fix Coverage / Assumptions

The original fix (commit `97d541f3`, included in Gradio 6.16.0) changed `FileExplorer.preprocess()` to use the component’s existing `_safe_join()` helper instead of raw `os.path.join` + `os.path.normpath`. `_safe_join()` delegates to `gradio.utils.safe_join()`, which:

- Normalizes the path with `posixpath.normpath()`.
- Raises `InvalidPathError` if the normalized path is absolute, equals `..`, or starts with `../`.
- Returns `os.path.join(root_dir, normalized_path)` only after validation passes.

The fix assumes that all attacker-controlled paths entering the `FileExplorer` component flow through either `preprocess()` (for submitted selections) or `ls()` (for directory browsing). It is correct: both methods now use `_safe_join()`. The `postprocess()` path and developer-supplied `value` do not accept untrusted API input.

### What the fix does NOT cover

- The fix is specific to `FileExplorer`. Separate Gradio path-handling issues (e.g., historical `/file` route traversals) involve different sinks and are not bypasses of this CVE.
- The fix does not resolve symlink attacks inside `root_dir`; that is a different threat vector.

## Variant / Alternate Trigger

### Entry point

All payloads are sent via the same unauthenticated HTTP endpoint:

```
POST /gradio_api/run/predict/
Content-Type: application/json
```

with a JSON body whose `data` array contains the serialized `FileExplorerData` value.

### Tested payloads

| Variant | Payload (`data` array) | Vulnerable commit (`0d670adf`) | Fixed commit (`97d541f3`) |
|---|---|---|---|
| Original single-level traversal | `[[["..", "gradio_secret.txt"]]]` | Leaks secret (200) | Blocked (500) |
| Absolute path | `[[["/tmp/gradio_secret.txt"]]]` | Leaks secret (200) | Blocked (500) |
| Deep relative traversal | `[[["..", "..", "tmp", "gradio_secret.txt"]]]` | Leaks secret (200) | Blocked (500) |

### Code path

1. `gradio/routes.py` receives the POST request and routes it to the prediction function.
2. The input is deserialized into `FileExplorerData` (`root: list[list[str]]`).
3. `FileExplorer.preprocess()` (lines 149–156 in `gradio/components/file_explorer.py`) resolves each inner list through `os.path.join(self.root_dir, *file)` + `os.path.normpath()` on the vulnerable commit, or through `_safe_join()` on the fixed commit.
4. The resolved path is passed to the user-defined `read_file` function, which reads it with `open()`.

## Impact

- **Product / component:** `gradio` Python package, `gradio/components/file_explorer.py`, `FileExplorer` component.
- **Affected versions (as tested):** Gradio before 6.16.0 (vulnerable commit `0d670adf`).
- **Fixed version:** Gradio 6.16.0 (commit `97d541f3`).
- **Risk:** High on the vulnerable version. Any unauthenticated caller can read arbitrary files readable by the Gradio process using either absolute or relative traversal payloads.

## Impact Parity

- **Disclosed / claimed maximum impact:** Arbitrary file read / information disclosure (`info_leak`) via directory traversal.
- **Reproduced impact from this variant run:** All three payloads returned the secret file contents on the vulnerable commit, demonstrating the same `info_leak` impact through a different input shape.
- **Parity:** `full` on the vulnerable commit; `none` on the fixed commit because the patch blocks the payloads.
- **Not demonstrated:** No bypass of the fixed commit; no code execution or write primitive.

## Root Cause

The same root cause drives all tested payloads: `FileExplorer.preprocess()` resolves attacker-supplied path segments with `os.path.join` + `os.path.normpath` without validating that the resolved path stays within `root_dir`. Because `os.path.join` discards the base directory when an absolute segment is present and `os.path.normpath` collapses `..` segments, the attacker can escape the intended sandbox. The fix replaces that unsafe resolution with `_safe_join()`/`safe_join()`, which rejects the resulting absolute or `../`-prefixed paths.

- **Fix commit:** `97d541f3d5fd05b2587a69ecc94b68fe5d2d7004`
- **Latest inspected commit:** `a34843eda` (still uses the same `_safe_join()` hardening)

## Reproduction Steps

Run the stage-specific reproduction script:

```bash
bash bundle/vuln_variant/reproduction_steps.sh
```

The script:

1. Reads the prepared project cache (`bundle/project_cache_context.json`) to locate the Gradio repo and venv.
2. Starts a small Gradio app with a `FileExplorer` input on `http://127.0.0.1:17863` for the vulnerable commit.
3. Sends the three payloads above to `/gradio_api/run/predict/` and records whether the secret file contents are returned.
4. Repeats the same tests against the fixed commit.
5. Restores the repo to the fixed commit and exits `1` (no bypass) or `0` (if a payload ever leaks on the fixed commit).

Expected output:

- Vulnerable commit: all three payloads return `200` with `PRUVA_SECRET_12345` in the `data` field.
- Fixed commit: all three payloads return `500` with no secret leakage.

## Evidence

- Main log: `bundle/logs/vuln_variant/reproduction_steps.log`
- Per-test server logs: `bundle/logs/vuln_variant/server_{original_traversal,absolute_path,deep_traversal}_{0d670adf,97d541f3}.log`
- Generated verdict: `bundle/vuln_variant/validation_verdict.json`
- Runtime manifest: `bundle/vuln_variant/runtime_manifest.json`
- Source identity: `bundle/vuln_variant/source_identity.json`
- Root-cause equivalence: `bundle/vuln_variant/root_cause_equivalence.json`

Key excerpt from the vulnerable run (absolute-path payload):

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

Key excerpt from the fixed run (same payload):

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

## Recommendations / Next Steps

- The existing Gradio 6.16.0 fix is sufficient for this component and these payloads. No additional code change is required.
- Maintain regression tests that exercise `FileExplorer.preprocess()` with absolute paths, single `..` segments, and multi-level `..` chains, asserting that `InvalidPathError` is raised in each case.
- For defense in depth, continue running the Gradio process with minimal filesystem permissions and keep secrets outside the process’s readable paths.

## Additional Notes

- **Idempotency:** The reproduction script was run twice consecutively and produced the same results both times.
- **Environment:** The script runs in the existing project-cache venv (Python 3.14) with `_frontend=False` because the source checkout does not contain built frontend assets. The API endpoint under test is the real production path.
- **Limitations:** Only POSIX payloads were tested; Windows-specific separator behaviors were not exercised because the runtime environment is Linux.
