Weblate · Report #4037976
Incomplete VCS metadata exclusion: `.svn` / `.bzr` bypass CVE-2026-61792 due to missing `VCS_METADATA_DIRS` entries.
How it was found
Weakness
Recommend: CWE-693: Protection Mechanism Failure (the CVE-2026-61792 protection mechanism — VCS-metadata exclusion — is incomplete: the VCS_METADATA_DIRS allowlist-equivalent blacklist only covers .git/.hg, not .svn/.bzr).
Affected assets / versions
- Repo:
WeblateOrg/weblate, branchmain(verified 2026-09-17, post-fix HEAD containing commits299fb28/33a9acb). - Releases: every release carrying the CVE-2026-61792 fix ships the incomplete set — verified at tags
weblate-2026.7(first fixed release) andweblate-2026.9.1(latest):VCS_METADATA_DIRS = frozenset((".git", ".hg"))at line 36. Affected: 2026.7, 2026.7.1, 2026.8, 2026.8.1, 2026.9, 2026.9.1, and main. (Tagweblate-5.17predates the June fix and has no such filter.) - File:
weblate/utils/files.py, line 36:
VCS_METADATA_DIRS = frozenset((".git", ".hg"))
- Consumers of the incomplete set:
weblate/utils/views.py:780and:797—_is_download_path()(ZIP download pre-check + post-resolve check)weblate/vcs/base.py:898—resolve_symlinks()weblate/utils/validators.py:372— path validationweblate/trans/tasks.py:985— backupweblate/trans/discovery.py:629— component discovery
Summary
The June 2026 symlink/containment hardening for the translation ZIP download (GHSA-xwj4-fp82-r2rj / CVE-2026-61792, commits 299fb28 + 33a9acb) introduced VCS_METADATA_DIRS in weblate/utils/files.py to strip VCS metadata directories from downloaded archives. The set only contains (".git", ".hg"). Subversion (.svn) and Bazaar (.bzr) metadata are missing, even though:
- Weblate documents Subversion as an officially supported VCS (
docs/vcs.rst→ "Subversion", incl. a "Subversion credentials" section), and - Weblate's own copy logic treats
.svn/.bzras VCS metadata:weblate/trans/component_copy.py:109usesshutil.ignore_patterns(".git", ".hg", ".svn", ".bzr").
As a result, is_vcs_metadata_path() returns False for .svn/... and .bzr/..., so any .svn or .bzr tree present inside a component (committed by any user with push access, or accidentally committed during an SVN→Git migration — a well-known real-world occurrence) is bundled verbatim into the downloaded ZIP, including the SQLite working-copy database wc.db (repository layout, URLs, revision mappings) and any .svn/auth/ credential-cache files that found their way into the tree. Git and Mercurial equivalents are correctly excluded — the differential is directly observable with a one-line expression (see Step 3).
I am deliberately reporting this as an incomplete-hardening / defense-in-depth gap rather than the credential-theft scenario that motivated my initial test: the containment layer added in the same fix blocks cross-component symlink escapes (verified, see Step 4), Weblate's SVN backend is git-svn (no .svn directory is created in working trees), and the SVN auth cache lives in DATA_DIR/home/.subversion — outside component roots. The concrete, reproducible impact is: VCS metadata that the June 2026 fix was designed to exclude still ships in ZIP downloads when a component contains .svn/.bzr data, which is a completeness gap in a security-fix mechanism and is inconsistent with the project's own ignore-set in component_copy.py.
Steps to reproduce
Prerequisites: Python 3.10+, pip install django translation-finder, and the Weblate main source tree. The attached PoC (poc_cve_2026_61792_svn_metadata_leak.py) loads the real weblate/utils/files.py from main via importlib and runs a verbatim copy of _is_download_path() / _get_containing_root() / _get_allowed_roots() / iter_files() from weblate/utils/views.py (lines 755–808), mirroring the exact call made by the API download endpoint (weblate/api/views.py:670-676: zip_download(filename, [filename]) for directory requests, root = component directory, allowed_roots=None).
Step 1 — Confirm the shipped filter
git clone https://github.com/WeblateOrg/weblate.git
grep -n "VCS_METADATA_DIRS" weblate/utils/files.py
# 36: VCS_METADATA_DIRS = frozenset((".git", ".hg"))
Step 2 — Confirm the inconsistency inside Weblate itself
sed -n '105,115p' weblate/trans/component_copy.py
# 109: ignore_vcs_metadata = shutil.ignore_patterns(".git", ".hg", ".svn", ".bzr")
Weblate's own copy routine already knows .svn and .bzr are VCS metadata; the security filter does not.
Step 3 — Differential check at the filter level
from weblate.utils.files import is_vcs_metadata_path
print(is_vcs_metadata_path(".git/config")) # True -> excluded from ZIP
print(is_vcs_metadata_path(".hg/hgrc")) # True -> excluded from ZIP
print(is_vcs_metadata_path(".svn/wc.db")) # False -> INCLUDED in ZIP
print(is_vcs_metadata_path(".svn/auth/svn.simple/x")) # False -> INCLUDED in ZIP
print(is_vcs_metadata_path(".bzr/README")) # False -> INCLUDED in ZIP
Step 4 — Run the attached PoC (full download-path walk + ZIP build)
python poc_cve_2026_61792_svn_metadata_leak.py
Observed output (main branch, real code):
[shipped logic] is_vcs_metadata_path() verdicts:
.git/config -> True
.hg/hgrc -> True
.svn/wc.db -> False
.svn/auth/svn.simple/..-> False
.bzr/README -> False
[result] entries in the generated ZIP (arcname relative to component root):
+ .bzr/README
+ .svn/auth/svn.simple/1234abcd
+ .svn/entries
+ .svn/wc.db
+ leak.db
+ translations/hello.po
[summary]
INCLUDED translations/hello.po normal content (expected)
EXCLUDED .git/config git metadata — blocked by is_vcs_metadata_path()
EXCLUDED .git/objects/commit git objects — blocked
EXCLUDED .hg/hgrc hg metadata — blocked
INCLUDED .svn/entries SVN metadata — NOT blocked (gap)
INCLUDED .svn/wc.db SVN working-copy DB — NOT blocked (gap)
INCLUDED .svn/auth/svn.simple/1234abcd SVN credential cache — NOT blocked (gap)
INCLUDED .bzr/README Bazaar metadata — NOT blocked (gap)
INCLUDED leak.db -> .svn/wc.db symlink to .svn metadata — NOT blocked (gap)
EXCLUDED gitleak.db -> .git/config symlink to .git metadata — blocked (control)
EXCLUDED escape -> ../beta/secret.txt cross-component symlink — blocked by containment (control)
[impact proof] bytes of .svn/wc.db recovered through the symlinked entry:
'leak.db' -> 8192 bytes: SQLite header b'SQLite format 3\x00' ...
Key observations:
- The
.svntree (entries,wc.db,auth/svn.simple/*) and the.bzrtree are included in the ZIP;.git/.hgare excluded. - A symlink inside the component pointing at
.svn/wc.db(leak.db) is included, and the ZIP entry yields the real SQLite bytes ofwc.db— while the identical trick against.git/config(gitleak.db) is correctly blocked. - The containment layer holds: a cross-component symlink (
escape -> ../beta/secret.txt) is still excluded by_get_containing_root()+is_path_within_resolved_directory(). This confirms scope: the gap is the missing.svn/.bzrentries in the metadata filter, not a bypass of the symlink containment.
Step 5 — Verify the one-line fix closes the gap
The PoC re-runs the identical walk with VCS_METADATA_DIRS = frozenset((".git", ".hg", ".svn", ".bzr")):
EXCLUDED .svn/entries
EXCLUDED .svn/wc.db
EXCLUDED .svn/auth/svn.simple/1234abcd
EXCLUDED .bzr/README
EXCLUDED leak.db
EXCLUDED gitleak.db
EXCLUDED escape
INCLUDED translations/hello.po
All assertions: PASS
Suggested fix
weblate/utils/files.py, line 36 — one line:
VCS_METADATA_DIRS = frozenset((".git", ".hg", ".svn", ".bzr"))
This matches the set already used by weblate/trans/component_copy.py:109 (shutil.ignore_patterns(".git", ".hg", ".svn", ".bzr")). is_vcs_metadata_path() casefolds its input, so case variants (.SVN, .SVn) are covered automatically. A regression test asserting the .svn/.bzr ZIP-exclusion parity with .git/.hg would lock this in (existing coverage only exercises .git: weblate/utils/tests/test_files.py; there is currently no test for .svn/.bzr).
References
- GHSA-xwj4-fp82-r2rj / CVE-2026-61792 — original symlink-leak hardening (commits
299fb28,33a9acb) weblate/utils/files.py:36—VCS_METADATA_DIRS = frozenset((".git", ".hg"))weblate/utils/views.py:780,797—_is_download_path()callsweblate/trans/component_copy.py:109—ignore_vcs_metadata = shutil.ignore_patterns(".git", ".hg", ".svn", ".bzr")weblate/api/views.py:670-676—zip_download(filename, [filename])directory-download call pathweblate/utils/views.py:840-855,920-927— UIzip_download()call sitesdocs/vcs.rst→ "Subversion" (git-svn backend; "Subversion credentials" note)weblate/vcs/base.py:898,weblate/utils/validators.py:372,weblate/trans/tasks.py:985,weblate/trans/discovery.py:629— other consumers of the incomplete set
Attachments
poc_cve_2026_61792_svn_metadata_leak.py— runnable PoC (loads realweblate/utils/files.pyfrommain; verbatim_is_download_path/iter_filesfromweblate/utils/views.py; differential.gitvs.svn/.bzrZIP walk; fix check)poc_run_output.txt— full captured output of the PoC run
Impact
- Any user with access to a component's ZIP download (the
translation.downloadpermission — the same threat model the June 2026 advisory was fixed for) can obtain a component's full Subversion or Bazaar metadata if such data exists in the tree: repo layout and URL (wc.dbREPOSITORY table), working-copy state, and any.svn/auth/svn.simplecredential-cache entries that were ever committed into the tree. - This is a completeness gap in a shipped security fix: the June 2026 hardening explicitly guarantees VCS metadata is stripped from downloads, and it silently fails for two of the four VCS metadata types the project itself recognizes in
component_copy.py:109. Themainbranch currently ships this state; the same incomplete set also feedsvcs/base.py:898,validators.py:372,tasks.py:985, anddiscovery.py:629, so symlink resolution, path validation, backup, and component discovery all share the inconsistency. - I am not claiming remote credential theft on stock installs: Weblate's SVN support is git-svn (
docs/vcs.rst), which creates no.svnfolder in the checkout (metadata lives under.git/svn, already excluded), and the SVN auth cache (DATA_DIR/home/.subversion/auth) lives outside any component root. Reproducible impact is the metadata-disclosure/incompleteness above; on self-hosted deployments that previously used direct SVN working copies or that committed.svn/.bzrdirectories into Git (a documented, common migration accident), the leaked ZIP contents additionally include the storedsvn.simpleauth blobs for that repository.
Suggested severity: Low–Medium (needs an authenticated user, needs attacker-influenced repo content, disclosure is within the same component root; overriding factor is that it defeats a security mechanism explicitly introduced by CVE-2026-61792 and is trivially fixable).