HexBrain

Weblate · Report #4040149

wlc: Project-wide download fails part-way and leaves stale archives on disk; filesystem errors surface as raw Python tracebacks.

Validstatus
lowseverity
14upvotes
30 Oct 2026disclosed

How it was found

Summary

wlc (the Weblate command-line client, https://github.com/WeblateOrg/wlc) does not handle filesystem errors anywhere in the CLI. This causes two related problems:

  1. Partial download state (CWE-755). wlc download <project> writes one archive per component in a loop. If a component fails after earlier ones succeeded, the already-written archives stay on disk, the command exits 1, and the CLI never reports which archives were written and which failed. Automation that consumes the output directory (CI/CD publish pipelines) cannot distinguish a complete result from a partial one and will pick up stale archives.
  2. Raw tracebacks on stderr (CWE-209). Any filesystem error (unwritable output directory, output path colliding with an existing file, missing upload input) propagates out of main() as an uncaught OSError and prints a full Python traceback instead of the single-line Error: <message> pattern used by every other failure mode. The traceback exposes the operator's local absolute paths and username (e.g. /home/<user>/...) and internal package frames.

Verified against the latest release 2.2.0 (PyPI) and the current main branch (commit 1b865157), running as a non-root user against a minimal local stub of the Weblate API. <url>/<key> below are the API endpoint and token; <project>/<component>/<lang> are existing objects on the instance.

Steps To Reproduce

Step 1 — Project download fails part-way: stale archives left on disk

Setup: <project> has two components; the second one fails (its /file/ endpoint returns HTTP 500).

  1. Start with an empty output directory:
mkdir -p /tmp/dl && rm -f /tmp/dl/*
  1. Run the project download:
wlc --url <url> --key <key> download -o /tmp/dl <project>
  1. Observed (with component 2 failing):
$ wlc --url http://127.0.0.1:18080/api --key TESTKEY --allow-insecure-http download -o /tmp/dl proj
downloaded translations for component: proj/comp1
Error: HTTP error 500: Internal Server Error {'detail': 'boom'}
$ echo $?
1
$ ls -la /tmp/dl/
-rw-rw-r--  1 user user  217 Sep 18 09:28 proj-comp1.zip

Expected: either a complete/validated set of archives, or a clear report of which components succeeded and which failed. Actual: component 1's archive remains on disk, no write summary is printed, exit code is 1. Downstream steps driven by && stop, but any step that ignores/tees the exit code or re-runs the pipeline continues with the stale proj-comp1.zip.

Step 2 — Unwritable output directory: raw PermissionError traceback

  1. Create an unwritable directory:
mkdir /tmp/wlc_ro && chmod 555 /tmp/wlc_ro
  1. Run a component download writing into it:
wlc --url <url> --key <key> download -o /tmp/wlc_ro/out <project>/<component>
  1. Expected: Error: [Errno 13] Permission denied: '/tmp/wlc_ro/out' on stderr, exit 1.

Actual:

Traceback (most recent call last):
  File "/home/<user>/.local/bin/wlc", line 7, in <module>
    sys.exit(main())
  File "/home/<user>/.local/lib/python3.13/site-packages/wlc/main.py", line 1121, in main
    command.run()
  File "/home/<user>/.local/lib/python3.13/site-packages/wlc/main.py", line 219, in _write_download_file
    path.parent.mkdir(exist_ok=True, parents=True)
  File "/usr/lib/python3.13/pathlib/_local.py", line 724, in mkdir
    os.mkdir(self, mode)
PermissionError: [Errno 13] Permission denied: '/tmp/wlc_ro/out'

Full traceback (multiple absolute paths including the username), then exit code 1.

Step 3 — Output path collides with an existing file: raw FileExistsError traceback

  1. Create a regular file named as the intended output directory:
touch out.zip
  1. Run a component download with -o out.zip (for project/component downloads -o is treated as a directory, without pre-validation):
wlc --url <url> --key <key> download -o out.zip <project>/<component>
  1. Expected: a clear Error: ... and exit 1.

Actual:

File ".../wlc/main.py", line 219, in _write_download_file
    path.parent.mkdir(exist_ok=True, parents=True)
File "/usr/lib/python3.13/pathlib/_local.py", line 724, in mkdir
    os.mkdir(self, mode)
FileExistsError: [Errno 17] File exists: 'out.zip'

Step 4 — Upload with missing input file: raw FileNotFoundError traceback

  1. Run an upload referencing a non-existent file:
wlc --url <url> --key <key> upload -i /tmp/does_not_exist.po <project>/<component>/<lang>
  1. Expected: Error: ... and exit 1.

Actual:

File ".../wlc/main.py", line 1004, in run
    with open(self.args.input, "rb") as handle:
FileNotFoundError: [Errno 2] No such file or directory: '/tmp/does_not_exist.po'

Vulnerability Details

  • Affected code (wlc/main.py):
    • main() — exception handlers catch WLCConfigurationError, WeblateDeniedError, RequestException, (CommandError, WeblateException), but not OSError.
    • Download.download_component() / _write_download_file() (~line 211/219) — path.parent.mkdir() and os.open() raise uncaught PermissionError / FileExistsError.
    • Upload.run() (~line 996/1004) — open(self.args.input, "rb") raises uncaught FileNotFoundError.
  • Present in release 2.2.0 and in main branch 1b865157 (same code, line numbers shifted to 216/996).
  • The translation-download -o <existing-dir> case is not affected: since 2.2.0 _write_download_file() raises CommandError("Refusing to write downloaded file to non-regular path: ..."), so that path is already handled.

Weakness

  • CWE-755: Improper Handling of Exceptional Conditions (primary)
  • CWE-209: Generation of Error Message Containing Sensitive Information (secondary)

Remediation

  1. Add an OSError handler in main() next to the existing ones:
except OSError as error:
    print_stderr(f"Error: {error}")
    return 1

Note: the RequestException handler uses exit code 10; this handler should use 1, consistent with CommandError.

  1. In download_component(), validate the output path before calling _write_download_file(): if --output exists and is not a directory, raise a concise CommandError. The existing non-regular-path guard inside _write_download_file() runs after mkdir and does not cover the file-as-parent case (Step 3).

  2. For project-wide downloads, print a summary of written files (or stage writes and rename on success) so automation can distinguish complete from partial results (Step 1).

References

Impact

  • CWE-755 (primary): failed project downloads leave a mixture of fresh and stale archives with no written-files report. In CI/CD pipelines that publish the output directory, incomplete translation bundles can reach production without detection, or a build fails on one component while another component's archive quietly persists and gets reused on the next run. Integrity/availability problem for the automation layer.
  • CWE-209 (secondary): raw tracebacks expose the operator's local absolute paths and username into stderr. In environments where stderr is captured and persisted (CI logs, journald, monitoring), that data lands where it does not belong.

This report was disclosed publicly on HackerOne and belongs to its author and the program. See the original