Reports
MEDIA[EXPOSICIÓN DE DATOS]#4037976

Weblate: el filtro de metadatos VCS del ZIP de descarga olvidaba .svn y .bzr tras el parche de CVE-2026-61792

El parche de CVE-2026-61792 en Weblate quitaba .git y .hg de los ZIP de descarga, pero no .svn ni .bzr. Si un componente los contenía, su contenido (incluido wc.db) salía en el ZIP.

Resumen
Resumen en castellano de un reporte público, no una traducción literal. El código y los comandos se mantienen como en el original.

Resumen

En junio de 2026 Weblate endureció la descarga en ZIP de las traducciones de un componente (aviso GHSA-xwj4-fp82-r2rj / CVE-2026-61792, commits 299fb28 y 33a9acb). Una de las piezas de ese arreglo es la constante VCS_METADATA_DIRS, en weblate/utils/files.py, que sirve para que los directorios de metadatos del sistema de control de versiones no acaben dentro del ZIP. El problema es que solo contemplaba Git y Mercurial:

VCS_METADATA_DIRS = frozenset((".git", ".hg"))

Subversion (.svn) y Bazaar (.bzr) se quedaron fuera. Por eso is_vcs_metadata_path() devolvía False para rutas como .svn/wc.db, y cualquier árbol .svn o .bzr que hubiera dentro de un componente se empaquetaba tal cual en la descarga. Ocurría incluso a través de un enlace simbólico interno que apuntara a esos ficheros, mientras que el mismo truco contra .git sí se bloqueaba.

La propia Weblate ya trataba esos directorios como metadatos en otra parte del código: weblate/trans/component_copy.py:109 usa shutil.ignore_patterns(".git", ".hg", ".svn", ".bzr"). Además, Subversion figura en la documentación (docs/vcs.rst) como sistema soportado.

Según el investigador, estaban afectadas todas las versiones que incluían el arreglo original: 2026.7, 2026.7.1, 2026.8, 2026.8.1, 2026.9, 2026.9.1 y la rama main. La 5.17 es anterior al arreglo y no tenía ningún filtro de este tipo. El mismo conjunto incompleto lo usan también otras partes del código:

  • weblate/utils/views.py:780 y :797: _is_download_path(), la comprobación de la descarga ZIP antes y después de resolver la ruta.
  • weblate/vcs/base.py:898: resolve_symlinks().
  • weblate/utils/validators.py:372: validación de rutas.
  • weblate/trans/tasks.py:985: copias de seguridad.
  • weblate/trans/discovery.py:629: descubrimiento de componentes.

El investigador lo presenta expresamente como un endurecimiento incompleto y no como un robo de credenciales en instalaciones estándar. La contención de enlaces simbólicos entre componentes que añadió el mismo parche sigue funcionando, y el soporte de Subversion de Weblate funciona con git-svn, que no crea .svn en la copia de trabajo.

El reporte se envió el 17 de septiembre de 2026, consta como resuelto y se divulgó el 30 de octubre de 2026. El CVE que aparece en la ficha, CVE-2026-61792, es el del fallo original que este reporte completa.

Pasos de reproducción

Requisitos: Python 3.10+, pip install django translation-finder y el código fuente de Weblate (main). La prueba de concepto del investigador (poc_cve_2026_61792_svn_metadata_leak.py) carga el weblate/utils/files.py real y ejecuta una copia literal de _is_download_path(), _get_containing_root(), _get_allowed_roots() e iter_files() de weblate/utils/views.py (líneas 755–808). Imita la llamada que hace el endpoint de descarga de la API (weblate/api/views.py:670-676, zip_download(filename, [filename]), con la raíz en el directorio del componente y allowed_roots=None).

  1. Comprobar el filtro que se distribuye:
git clone https://github.com/WeblateOrg/weblate.git
grep -n "VCS_METADATA_DIRS" weblate/utils/files.py
# 36: VCS_METADATA_DIRS = frozenset((".git", ".hg"))
  1. Ver que la rutina de copia de Weblate sí conoce .svn y .bzr:
sed -n '105,115p' weblate/trans/component_copy.py
# 109: ignore_vcs_metadata = shutil.ignore_patterns(".git", ".hg", ".svn", ".bzr")
  1. Comparar el comportamiento del filtro con cada tipo de metadato:
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
  1. Ejecutar la prueba de concepto completa, que recorre el componente y genera el ZIP:
python poc_cve_2026_61792_svn_metadata_leak.py

Salida con el código de main:

[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' ...

Los árboles .svn y .bzr entran en el ZIP, y .git y .hg no. El enlace leak.db, que apunta a .svn/wc.db, devuelve los bytes reales de la base de datos SQLite, mientras que gitleak.db, que apunta a .git/config, se bloquea. El enlace que intenta salir a otro componente (escape) sigue bloqueado: el fallo está solo en la lista de directorios, no en la contención.

  1. Repetir la prueba con la lista corregida (VCS_METADATA_DIRS = frozenset((".git", ".hg", ".svn", ".bzr"))) y ver que todo queda excluido salvo translations/hello.po:
  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

Impacto

  • Un usuario con permiso de descarga ZIP del componente (translation.download, el mismo modelo de amenaza que el aviso de junio) podía obtener todos los metadatos de Subversion o Bazaar que hubiera en el árbol: la estructura y la URL del repositorio (tabla REPOSITORY de wc.db), el estado de la copia de trabajo y cualquier entrada de caché de credenciales .svn/auth/svn.simple que se hubiera llegado a subir al repositorio.
  • Para eso tiene que haber .svn o .bzr dentro del componente: porque alguien con permiso de escritura los suba, o por un despiste en una migración de SVN a Git, cosa que según el investigador pasa a menudo.
  • El investigador no afirma que se puedan robar credenciales en una instalación estándar: git-svn guarda sus metadatos en .git/svn, que ya se excluye, y la caché de autenticación de SVN está en DATA_DIR/home/.subversion/auth, fuera de los componentes. El riesgo real es que un mecanismo de seguridad publicado como arreglo de un CVE no cumplía lo que prometía con dos de los cuatro tipos de metadatos que el propio proyecto reconoce.
  • El investigador propuso una severidad baja-media (hace falta un usuario autenticado y contenido controlado en el repositorio, y la fuga no sale de la raíz del componente). HackerOne la registra como media.

Remediación

El investigador propone completar la lista en weblate/utils/files.py, línea 36, para igualarla con la de component_copy.py:

VCS_METADATA_DIRS = frozenset((".git", ".hg", ".svn", ".bzr"))

Como is_vcs_metadata_path() normaliza mayúsculas y minúsculas, variantes como .SVN quedarían cubiertas. También recomienda añadir un test de regresión que compruebe que .svn y .bzr se excluyen igual que .git y .hg, porque weblate/utils/tests/test_files.py solo prueba .git. El reporte consta como resuelto, pero el texto publicado no detalla qué cambio se aplicó finalmente.

Qué aprender de este caso

  • Si una aplicación define en dos sitios qué cuenta como «metadatos VCS» (aquí VCS_METADATA_DIRS y el ignore_patterns de component_copy.py), compara las dos listas: la diferencia entre ellas suele marcar el hueco del filtro de seguridad.
  • Al revisar un parche de seguridad basado en una lista negra, prueba con los casos que la lista no menciona: otros sistemas de control de versiones, otros nombres de fichero sensibles y enlaces simbólicos que apunten a ellos.
  • Al programar, centraliza esa lista en una única constante que usen todos los consumidores (descarga, copia, copias de seguridad, validación) y cúbrela con un test por cada directorio, no solo por .git.
  • En funciones que empaquetan un directorio para descargarlo, pregúntate siempre qué puede haber subido alguien a ese directorio, aunque la instalación normal no lo cree (como .svn en un repositorio migrado).