CVE-2026-49119: Verified Repro With Script Download
CVE-2026-49119: Gradio FileExplorer path traversal
CVE-2026-49119 is verified against gradio-app/gradio · github. Affected versions: < 6.16.0. Fixed in 6.16.0. Vulnerability class: Path Traversal. This high reproduction includes runnable sandbox proof, artifacts, and a plain-text agent view under REPRO-2026-00246.
What Is CVE-2026-49119?
CVE-2026-49119 is a high-severity path traversal vulnerability (CWE-22) in Gradio's FileExplorer component that lets an unauthenticated attacker read arbitrary files on the Gradio server host. Pruva reproduced it (reproduction REPRO-2026-00246).
CVE-2026-49119 Severity & CVSS Score
CVE-2026-49119 is rated high severity, with a CVSS base score of 7.5 out of 10.
High — serious impact or readily exploitable. Prioritize remediation.
Affected gradio-app/gradio Versions
gradio-app/gradio · github versions < 6.16.0 are affected.
How to Reproduce CVE-2026-49119
pruva-verify REPRO-2026-00246 curl -O https://pruva.dev/api/v1/reproductions/REPRO-2026-00246/artifacts/bundle/repro/reproduction_steps.sh && chmod +x reproduction_steps.sh && ./reproduction_steps.sh Proof of Reproduction for CVE-2026-49119
- reached the target end-to-end
- on the real production code path
- high confidence
- the upstream fix blocks the same trigger
FileExplorer payload with ../ traversal segments
- /gradio_api/run/predict/ of a running Gradio app with FileExplorer input
Alternate FileExplorer payloads (absolute path and multi-level relative traversal) trigger the same unvalidated path-join sink in FileExplorer.preprocess() on Gradio <6.16.0. The patched commit (6.16.0) blocks all tested payloads via safe_join, so this is not a bypass.
How the agent worked
Root Cause and Exploit Chain for CVE-2026-49119
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.
- Product / component:
gradioPython package, specificallygradio/components/file_explorer.py(FileExplorercomponent). - 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 configuredroot_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:
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
- Run
bundle/repro/reproduction_steps.sh. The script is self-contained and works from any directory; it setsPRUVA_ROOTautomatically if not provided. - 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 aFileExplorerinput rooted at/tmp/gradio_root, and uses_frontend=Falseso the server can run without a built frontend. - Waits until the
/gradio_api/infohealth 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_12345read 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).
- Clones or reuses the Gradio repository in the durable project cache (
- 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 theInvalidPathErrortraceback).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:
{
"data": ["PRUVA_SECRET_12345"],
"is_generating": false,
"duration": 0.0002281665802001953,
...
}
Key excerpt from the fixed run:
{
"error": "",
"visible": true
}
Recommendations / Next Steps
- Upgrade to Gradio 6.16.0 or later, which includes the
_safe_joinvalidation inFileExplorer.preprocess(). - Validate all user-provided paths against a known-safe root directory before using them for filesystem operations. Prefer library helpers such as
safe_joinover rawos.path.join/os.path.normpathfor user input. - Regression tests should be added that exercise
preprocess()with..and absolute-path payloads, asserting thatInvalidPathError(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=Falsebecause 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 vulnerableFileExplorer.preprocess()code path is reached.
Variant Analysis & Alternative Triggers for CVE-2026-49119
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):
- Absolute path payload:
[[["/tmp/gradio_secret.txt"]]]— theos.path.join(self.root_dir, *file)call discards the configuredroot_dirwhen the first segment is absolute and returns an arbitrary absolute path. - Deep relative traversal payload:
[[["..", "..", "tmp", "gradio_secret.txt"]]]— multiple..segments normalize to a path outsideroot_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
InvalidPathErrorif 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/fileroute 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
gradio/routes.pyreceives the POST request and routes it to the prediction function.- The input is deserialized into
FileExplorerData(root: list[list[str]]). FileExplorer.preprocess()(lines 149–156 ingradio/components/file_explorer.py) resolves each inner list throughos.path.join(self.root_dir, *file)+os.path.normpath()on the vulnerable commit, or through_safe_join()on the fixed commit.- The resolved path is passed to the user-defined
read_filefunction, which reads it withopen().
- Product / component:
gradioPython package,gradio/components/file_explorer.py,FileExplorercomponent. - 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_leakimpact through a different input shape. - Parity:
fullon the vulnerable commit;noneon 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 bundle/vuln_variant/reproduction_steps.sh
The script:
- Reads the prepared project cache (
bundle/project_cache_context.json) to locate the Gradio repo and venv. - Starts a small Gradio app with a
FileExplorerinput onhttp://127.0.0.1:17863for the vulnerable commit. - Sends the three payloads above to
/gradio_api/run/predict/and records whether the secret file contents are returned. - Repeats the same tests against the fixed commit.
- Restores the repo to the fixed commit and exits
1(no bypass) or0(if a payload ever leaks on the fixed commit).
Expected output:
- Vulnerable commit: all three payloads return
200withPRUVA_SECRET_12345in thedatafield. - Fixed commit: all three payloads return
500with 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):
{
"data": ["PRUVA_SECRET_12345"],
"is_generating": false,
"duration": ...
}
Key excerpt from the fixed run (same payload):
{
"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 thatInvalidPathErroris 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=Falsebecause 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.
CVE-2026-49119 Reproduction Transcript
The agent's step-by-step process — every tool call, every handoff, the moment the exploit fired.
Full session Replay every step — scrub the timeline or play it back.
Artifacts and Evidence for CVE-2026-49119
Scripts, logs, diffs, and output captured during the reproduction.
How to Fix CVE-2026-49119
Upgrade gradio-app/gradio · github to 6.16.0 or later.
FAQ: CVE-2026-49119
How does the CVE-2026-49119 path traversal work?
.. segments as the selected FileExplorer path; the vulnerable preprocess() implementation resolves the path outside root_dir and passes it to the user-defined prediction function, letting the attacker read files such as /etc/passwd or application secrets that are readable by the Gradio process.Which Gradio versions are affected by CVE-2026-49119, and where is it fixed?
How severe is CVE-2026-49119?
How can I reproduce CVE-2026-49119?
References for CVE-2026-49119
Authoritative sources for CVE-2026-49119 — official vulnerability databases and the upstream advisory. Pruva's reproduction verifies the issue firsthand; these are the primary records to corroborate it.