HexBrain

Weblate · Report #4037976

Incomplete VCS metadata exclusion: `.svn` / `.bzr` bypass CVE-2026-61792 due to missing `VCS_METADATA_DIRS` entries.

Validstatus
mediumseverity
21upvotes
30 Oct 2026disclosed

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, branch main (verified 2026-09-17, post-fix HEAD containing commits 299fb28 / 33a9acb).
  • Releases: every release carrying the CVE-2026-61792 fix ships the incomplete set — verified at tags weblate-2026.7 (first fixed release) and weblate-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. (Tag weblate-5.17 predates 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:780 and :797 — _is_download_path() (ZIP download pre-check + post-resolve check)
    • weblate/vcs/base.py:898 — resolve_symlinks()
    • weblate/utils/validators.py:372 — path validation
    • weblate/trans/tasks.py:985 — backup
    • weblate/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:

  1. Weblate documents Subversion as an officially supported VCS (docs/vcs.rst → "Subversion", incl. a "Subversion credentials" section), and
  2. Weblate's own copy logic treats .svn/.bzr as VCS metadata: weblate/trans/component_copy.py:109 uses shutil.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 .svn tree (entries, wc.db, auth/svn.simple/*) and the .bzr tree are included in the ZIP; .git/.hg are excluded.
  • A symlink inside the component pointing at .svn/wc.db (leak.db) is included, and the ZIP entry yields the real SQLite bytes of wc.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/.bzr entries 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() calls
  • weblate/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 path
  • weblate/utils/views.py:840-855,920-927 — UI zip_download() call sites
  • docs/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 real weblate/utils/files.py from main; verbatim _is_download_path/iter_files from weblate/utils/views.py; differential .git vs .svn/.bzr ZIP 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.download permission — 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.db REPOSITORY table), working-copy state, and any .svn/auth/svn.simple credential-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. The main branch currently ships this state; the same incomplete set also feeds vcs/base.py:898, validators.py:372, tasks.py:985, and discovery.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 .svn folder 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/.bzr directories into Git (a documented, common migration accident), the leaked ZIP contents additionally include the stored svn.simple auth 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).

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