# Patch Analysis: CVE-2026-49119 — Gradio FileExplorer Path Traversal

## Vulnerability

Gradio before 6.16.0 allowed unauthenticated API callers to escape the configured `root_dir` of a `FileExplorer` component and read arbitrary files. The root cause was in `gradio/components/file_explorer.py`, in the `FileExplorer.preprocess()` method, which built the selected path with:

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

Because `os.path.join` + `os.path.normpath` accept `..` segments and absolute-path prefixes, an attacker could supply either relative traversal or an absolute path as the selected file value and have the resolved path passed to the user-defined prediction function.

## What the Fix Changes

Fix commit: `97d541f3d5fd05b2587a69ecc94b68fe5d2d7004` (Gradio 6.16.0).

The patch replaces the unsafe join in `preprocess()` with the existing `FileExplorer._safe_join()` helper, which had already been used by the `ls()` server method:

```diff
-                return os.path.normpath(os.path.join(self.root_dir, *payload.root[0]))
+                return self._safe_join(payload.root[0])
...
         files = []
         for file in payload.root:
-            file_ = os.path.normpath(os.path.join(self.root_dir, *file))
+            file_ = self._safe_join(file)
             files.append(file_)
```

`_safe_join()` joins the user-provided path components into a single string, converts backslashes to forward slashes on Windows, and then delegates to `gradio.utils.safe_join()`. `safe_join()` is borrowed from Werkzeug and:

1. Normalizes the path with `posixpath.normpath()`.
2. Raises `InvalidPathError` if the normalized path is absolute (`os.path.isabs(...)` or starts with `/`), is exactly `..`, or starts with `../`.
3. Returns `os.path.join(root_dir, normalized_path)` only after validation succeeds.

## Fix Assumptions

- The only attacker-controlled paths entering the `FileExplorer` component arrive as `FileExplorerData` (a `list[list[str]]`) through `preprocess()` or `ls()`.
- Both `preprocess()` and `ls()` now use `_safe_join()`, so both entry points are covered by the same invariant.
- The `safe_join()` helper is sufficient for the target platform: it rejects absolute paths and any normalized relative path that starts with `../`, which prevents escaping `root_dir`.

## What the Fix Does NOT Cover

After source inspection and runtime testing, no additional unprotected paths were identified in `FileExplorer`:

- `postprocess()` only strips `root_dir` from paths returned by the developer’s function; it does not accept untrusted input.
- The `value` / `initial_value` of the component is set by the app developer, not by the API caller.
- The component-level `ls()` server method already used `_safe_join()` before the patch and was never vulnerable to this path traversal.

However, the fix is localized to `FileExplorer`. It does not address other Gradio path-handling issues such as the separate `/file` route path traversal (other CVEs), which involve a different sink and a different trust boundary. Those are not bypasses of CVE-2026-49119.

## Comparison: Before vs. After the Fix

| Input shape | Vulnerable (`0d670adf`) | Fixed (`97d541f3`) |
|---|---|---|
| Relative traversal: `[[["..","gradio_secret.txt"]]]` | Resolves outside `root_dir` and leaks file | `InvalidPathError` → 500, no leak |
| Absolute path: `[[["/tmp/gradio_secret.txt"]]]` | Discards `root_dir` and leaks file | `InvalidPathError` → 500, no leak |
| Deep traversal: `[[["..","..","tmp","gradio_secret.txt"]]]` | Resolves outside `root_dir` and leaks file | `InvalidPathError` → 500, no leak |
| Normal file: `[[["inside.txt"]]]` | Returns file under `root_dir` | Returns file under `root_dir` |

## Verdict on Completeness

The fix is **complete for the reported CVE**. It moves the same safe-path validation that already protected `ls()` into `preprocess()`, covering the exact code path that was reachable from the unauthenticated `/gradio_api/run/predict` endpoint. No bypass of the patched `safe_join` logic was found in the tested payloads, and the latest default-branch commit (`a34843eda`) still uses the same `_safe_join()` implementation.

## Threat Model / Security Policy Notes

Gradio’s `SECURITY.md` only provides a reporting contact and does not define any of the components as out-of-scope. The `FileExplorer` component is documented as allowing users to browse files on the machine hosting the Gradio app, so the security boundary is that the caller should only be able to access files within the configured `root_dir`. The fix is consistent with that boundary.
