Verified reproduction
CVE-2026-23535: wlc: Path traversal via unsanitized API slugs in download command
CVE-2026-23535 is verified against wlc · pip affected versions: wlc < 1.17.2 fixed version: wlc 1.17.2 vulnerability class: Path Traversal This high reproduction includes runnable sandbox proof, artifacts, and a plain-text agent view under REPRO-2026-00070.
Severity HIGH
Confidence HIGH
Reproduced in 20m 59s
Tool calls 216
Spend $3.97
Affected wlc < 1.17.2
Fixed in wlc 1.17.2
$
pruva-verify REPRO-2026-00070 or
curl -O https://pruva.dev/api/v1/reproductions/REPRO-2026-00070/artifacts/reproduction_steps.sh && chmod +x reproduction_steps.sh && ./reproduction_steps.sh Run in a VM or disposable container. This exploits a real vulnerability.
Multi-translation download could write to an arbitrary location when instructed by a crafted server.
## Summary
A malicious Weblate server can abuse the `wlc download` command’s multi-component workflow to write files outside the caller’s chosen output directory. The client trusts component metadata returned by the server and uses the embedded project slug verbatim when constructing the zip filename, allowing absolute paths to be honored and causing arbitrary file writes on the filesystem running wlc < 1.17.2.
## Impact
- **Package:** Weblate `wlc` CLI
- **Affected versions:** All releases prior to 1.17.2
- **Risk:** High – a crafted server (or MITM) can force the client to overwrite arbitrary files (e.g., `/etc/cron.d/...`) during `wlc download --multi`, leading to privilege escalation or system compromise on any machine that runs the command.
## Root Cause
`wlc.main.Download.download_component()` builds the destination path as `Path(output_dir) / f"{component.project.slug}-{component.slug}.zip"`. The values `component.project.slug` and `component.slug` come directly from API responses without sanitization. When a malicious server returns an absolute path such as `/tmp/weblate-owned` for the project slug, `pathlib.Path` treats it as an absolute path and drops the intended base directory, writing the downloaded archive to that attacker-chosen location. The issue is fixed upstream in https://github.com/WeblateOrg/wlc/pull/1128 (released in 1.17.2) which normalizes and validates slugs before using them in filesystem paths.
## Reproduction Steps
1. Run `repro/reproduction_steps.sh`.
2. The script provisions a Python venv, installs `wlc==1.17.1`, starts a mock Weblate HTTP server that serves a component listing whose project slug is `/tmp/weblate-owned`, and then executes `wlc download --output repro-output` against it.
3. Successful reproduction is indicated by creation of `/tmp/weblate-owned-absolute-component.zip` (outside the requested output directory) and logs stored in `logs/`.
## Evidence
- Log artifacts: `logs/mock_server.log`, `logs/wlc_download.log`, `logs/evidence.txt`.
- Key excerpt (`logs/evidence.txt`):
- `-rw-r--r-- 1 root root 58 ... /tmp/weblate-owned-absolute-component.zip`
- Intended output directory `repro-output/` remains empty, proving the write escaped the sandbox.
- Environment: Python 3.11 virtualenv with `wlc==1.17.1` on Ubuntu (container) as provisioned by the script.
## Recommendations / Next Steps
- Upgrade `wlc` clients to 1.17.2 or later, which sanitizes slugs before constructing output paths.
- For additional defense-in-depth, consider enforcing a jail by resolving all download outputs relative to the configured directory and rejecting absolute inputs from the server.
- Add regression tests that simulate malicious slugs to ensure future changes preserve the validation.
## Additional Notes
- `repro/reproduction_steps.sh` is idempotent: running it twice in succession succeeds, recreating the evidence and overwriting the malicious file each time.
- The mock server only implements the minimal endpoints needed for this PoC; real-world instances may require authentication, but the vulnerability manifests regardless once the client processes attacker-controlled component metadata.
The agent's step-by-step process — every tool call, every handoff, the moment the exploit fired. Phases: support triages the advisory · repro reproduces it · vuln_variant confirms the fix blocks it · judge verifies.
Loading session...
Scripts, logs, diffs, and output captured during the reproduction.
No artifacts available