Una raíz publicada debería funcionar como una frontera: un cliente autorizado a acceder a backups/equipo-a no debería poder alcanzar backups/equipo-b, incluso cuando la credencial de almacenamiento usada por el servidor tiene acceso a ambas ubicaciones. La CVE-2026-71309 rompía exactamente esa expectativa en el servidor REST compatible con restic que ofrece rclone.

En las versiones afectadas, una ruta de URL que comenzaba con ../ podía atravesar por encima de la raíz configurada por el operador. Según el backend, una solicitud al endpoint REST permitía leer, crear, sobrescribir o eliminar objetos hermanos y objetos ubicados en directorios superiores. El defecto de validación estaba en el middleware común del servidor, mientras que la explotación concreta dependía de cómo cada backend combinaba su raíz con la ruta remota recibida.

La vulnerabilidad se clasifica como CWE-22, con severidad alta y una puntuación CVSS v4.0 de 8,6. Las versiones de rclone desde la 1.40.0 hasta la 1.74.4 están afectadas, y la corrección está disponible en la versión 1.75.0. Estos datos constan en el advisory oficial GHSA-45pq-889g-fcgh.

La explotación exige un endpoint REST alcanzable, una subcarpeta de un backend publicada como raíz del servicio y credenciales del backend que puedan acceder al menos a algún objeto superior o hermano. El atacante también debe poder enviar solicitudes al servidor; en el vector CVSS publicado esto corresponde a privilegios bajos. No se requiere interacción de la víctima, condición de carrera ni conocimiento de una característica imprevisible del objetivo.

La vulnerabilidad no concede al proceso de rclone nuevos permisos en el backend. Permite que un cliente cruce la frontera lógica que el operador intentó crear al publicar solo una subcarpeta. El alcance final sigue limitado por la credencial del backend y por la semántica de rutas de ese almacenamiento.

Qué publica rclone serve restic

El comando rclone serve restic expone una API REST compatible con el protocolo que usa restic sobre un sistema de archivos de rclone. Ese sistema de archivos puede apuntar a un backend completo o a una subcarpeta. Por ejemplo:

rclone serve restic webdav:backups/equipo-a

En esta configuración, backups/equipo-a es la raíz que presenta el servicio. Aunque la credencial WebDAV pueda ver backups/equipo-b, un cliente de la API debería permanecer confinado a la raíz publicada. El servidor debe garantizar esa propiedad antes de transformar una ruta de URL en un nombre de objeto del backend.

Esta separación importa cuando una sola cuenta de almacenamiento atiende varios repositorios, clientes o trabajos de automatización. La autenticación del endpoint responde a la pregunta “¿quién puede llamar a la API?”. La raíz publicada responde a otra pregunta distinta: “¿qué objetos puede alcanzar ese cliente?”. Autenticar a un cliente no compensa un fallo en la segunda frontera.

Causa raíz: la canonicalización no es validación de contención

El componente vulnerable era el middleware independiente del backend WithRemote, ubicado en cmd/serve/restic/restic.go. Obtenía la ruta de la solicitud, recortaba las barras que la rodeaban e intentaba rechazar rutas no canónicas comparando la entrada con el resultado de path.Clean:

urlpath = strings.Trim(urlpath, "/")
if urlpath != "" && path.Clean(urlpath) != urlpath {
    http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
    return
}

La intención parece razonable: si la limpieza cambia el texto, la ruta contiene una construcción redundante o potencialmente peligrosa. Sin embargo, path.Clean normaliza una ruta; no certifica que una ruta relativa vaya a permanecer bajo una raíz que se une más adelante.

Considere los siguientes resultados:

path.Clean("../outside.txt")       = "../outside.txt"
path.Clean("../../outside.txt")    = "../../outside.txt"
path.Clean("a/../../outside.txt")  = "../outside.txt"

En los dos primeros ejemplos, el componente superior ya está al inicio de la ruta relativa. No hay un directorio previo que la función pueda eliminar, por lo que path.Clean conserva .. correctamente para su propósito de normalización. Como la entrada y la salida son iguales, la condición vulnerable acepta la solicitud.

La tercera ruta se comporta de otra forma. La limpieza elimina a/.. y cambia la cadena, lo que hace que la comparación detecte la diferencia y devuelva HTTP 400. Este comportamiento parcial puede volver el defecto menos evidente: una prueba con a/../../archivo parece confirmar que la travesía está bloqueada, mientras que la variante más directa ../archivo atraviesa la misma verificación.

El error conceptual es tratar la igualdad tras la canonicalización como una regla de validez. Una ruta canónica todavía puede estar prohibida. ../outside.txt es una representación canónica de una ruta relativa que sube un nivel, pero no es un nombre de objeto aceptable cuando la política exige el confinamiento por debajo de una raíz publicada.

De la URL al backend

Tras aceptar la ruta, WithRemote la almacenaba en el contexto de la solicitud:

ctx := context.WithValue(r.Context(), ContextRemoteKey, urlpath)
next.ServeHTTP(w, r.WithContext(ctx))

Los handlers posteriores confiaban en ese valor. Las operaciones de lectura resolvían el remoto mediante NewObject; las subidas enviadas con POST llegaban a operations.RcatSize; DELETE resolvía el objeto y llamaba a Remove. No había una segunda verificación común de contención antes de que el nombre del objeto llegara al backend.

El flujo de datos resultante era directo:

URL que contiene %2e%2e/archivo
        ↓ decodificación de la URL
../archivo
        ↓ WithRemote la acepta y la guarda en el contexto
handler GET, HEAD, POST o DELETE
        ↓
el backend combina su raíz con ../archivo
        ↓
la operación puede caer fuera de la raíz publicada

La codificación %2e%2e no es una variante independiente de la vulnerabilidad. Representa .. en la URL y permite observar el comportamiento después de que la capa HTTP decodifica la ruta. Los clientes y proxies pueden normalizar rutas antes de enviarlas, por lo que una reproducción controlada debe preservar la representación original de la URL.

Por qué el comportamiento del backend determina el resultado

La verificación defectuosa estaba en el código común del servidor, pero los backends no trataban el remoto aceptado de forma idéntica. Algunos unían la raíz del backend y el remoto con path.Join antes de codificar el nombre resultante para el protocolo de almacenamiento. En esos casos, .. eliminaba la subcarpeta publicada. Otros backends codificaban los componentes especiales como caracteres de nombre de archivo, lo que impedía la fuga de raíz en la configuración probada.

Para WebDAV, la construcción relevante era equivalente a:

subPath := path.Join(f.root, file)

Con valores concretos:

f.root = "served-root"
file   = "../outside-secret.txt"

path.Join("served-root", "../outside-secret.txt")
= "outside-secret.txt"

Cuando la solicitud llegaba al servidor WebDAV, ya parecía una operación ordinaria sobre outside-secret.txt. El servidor de almacenamiento remoto no sabía que rclone pretendía confinar al cliente a served-root.

Las pruebas publicadas confirmaron operaciones de lectura, escritura y eliminación fuera de la raíz con WebDAV, FTP, Memory y SFTP. El backend HTTP es de solo lectura, y allí se confirmó una lectura externa. En cada caso afectado, la ruta insegura aceptada por el servidor llegó a una implementación que preservaba o resolvía el componente superior de una forma capaz de cruzar la subcarpeta publicada.

No se produjo fuga de raíz en las configuraciones probadas con backend compatible con S3 ni con el backend local. Esas implementaciones codificaron .. como parte del nombre del objeto en lugar de interpretarlo como navegación hacia el directorio superior.

“No se observó fuga de raíz” no es una garantía universal de seguridad para toda configuración, versión y modo de codificación de un backend. Significa que la técnica analizada no cruzó la raíz en las pruebas publicadas. A la inversa, el defecto compartido en WithRemote no prueba automáticamente la explotabilidad contra todos los backends que soporta rclone.

Precondiciones y escenarios de impacto

El riesgo práctico aparece cuando el servicio ejecuta una versión anterior a la 1.75.0, publica una subcarpeta sin aislar la credencial exactamente en esa raíz, acepta solicitudes de un cliente potencialmente no confiable y usa un backend cuya semántica de rutas resuelve la entrada de forma afectada. La credencial también debe tener permiso sobre objetos superiores o hermanos; la vulnerabilidad no amplía los permisos otorgados por el sistema de almacenamiento.

Una disposición multiusuario ilustra el fallo de frontera:

backups/
├── cliente-a/
├── cliente-b/
└── configuracion-de-restauracion.json

Si el servicio publica solo backups/cliente-a, una solicitud a ../cliente-b/... puede cruzar la separación entre clientes. Una lectura compromete la confidencialidad de snapshots, índices o metadatos. Un POST puede crear o sobrescribir un objeto que consume otro trabajo. Un DELETE puede eliminar objetos externos cuando tanto la credencial como el backend lo permiten.

El impacto no se detiene necesariamente en el almacenamiento. Otro sistema puede interpretar más tarde un objeto sobrescrito como configuración, script, manifiesto o artefacto de restauración. Esa cadena depende del entorno: la CVE permite la modificación no autorizada del objeto, mientras que la ejecución o la confianza posterior dependen de otro componente.

La opción --append-only reduce parte del riesgo de sobrescritura y eliminación, pero no repara la validación. El advisory señala que las lecturas por travesía y la creación de objetos fuera de la raíz pueden seguir siendo posibles, junto con escenarios específicos de eliminación que involucran rutas de lock. Es una reducción parcial del impacto, no una remediación.

Cómo se manifestaba la explotación

En una instalación vulnerable, una solicitud podía preservar el componente superior en la URL usando su forma codificada. Una solicitud equivalente al ejemplo siguiente intentaba recuperar outside-secret.txt, ubicado un nivel por encima de la raíz publicada:

GET /%2e%2e/outside-secret.txt HTTP/1.1
Host: restic.example
Authorization: Basic …

Tras la decodificación, el servidor trabajaba con ../outside-secret.txt. En WebDAV, unir ese valor a served-root producía outside-secret.txt, de modo que el backend recibía una lectura ordinaria fuera de la subcarpeta prevista. Aplicar la misma idea a POST o DELETE podía crear, sobrescribir o eliminar objetos cuando el backend y la credencial permitían esas operaciones.

El comportamiento correcto es rechazar la ruta con HTTP 400 antes de invocar el backend. La diferencia entre las versiones vulnerable y corregida no es la apariencia final de la operación de almacenamiento, sino el punto en el que la ruta deja de aceptarse como un nombre de objeto válido.

Cómo funciona la corrección

La corrección está en la versión 1.75.0 de rclone. Reemplazó la comparación con path.Clean por una regla de validez explícita basada en io/fs.ValidPath. En lugar de preguntar si la limpieza cambió el texto, el servidor ahora pregunta si la ruta es un nombre relativo válido antes de que llegue a cualquier backend.

ValidPath acepta . como la representación especial de la raíz de un sistema de archivos. Para cualquier otro valor, exige una ruta relativa sin raíz, codificada como UTF-8 válido y sin componentes vacíos, . o ... El servidor maneja sus dos casos especiales de forma explícita: conserva la ruta vacía como la raíz legítima de la API y rechaza . como nombre de objeto.

El cambio corrige el problema en la frontera común, antes de que cualquier backend vea el remoto. Esta ubicación importa. Compensar de forma independiente en cada backend invitaría a comportamientos inconsistentes, nuevas implementaciones vulnerables y operaciones olvidadas.

La release también añadió cobertura de regresión para GET, HEAD, POST y DELETE, ejercitando entradas como .., ../, ../../, ../outside-secret.txt, %2e%2e/outside-secret.txt, formas de travesía interior, . y ./inside.txt contra un backend que resuelve los componentes superiores al unir rutas, y verificando tanto la respuesta HTTP 400 como la integridad de los objetos externos.

Remediación y defensa en profundidad

La corrección principal es actualizar a rclone 1.75.0 o posterior. Reinicie los procesos correspondientes y confirme la versión efectivamente cargada. Las imágenes inmutables, los hosts con varias instalaciones y los servicios que conservan un binario antiguo en memoria pueden hacer que una actualización aparentemente completa no alcance la instancia activa.

Si una actualización inmediata es imposible, la exposición puede reducirse restringiendo el endpoint a las redes, identidades y trabajos que lo requieran estrictamente. La credencial del backend debería comenzar exactamente en la raíz publicada, y clientes o repositorios distintos deberían usar recursos compartidos, buckets o cuentas diferentes. Los permisos de escritura y eliminación también deberían retirarse cuando un flujo solo requiera lecturas.

Estas medidas limitan el alcance posible, pero no reparan la validación. La autenticación, un proxy inverso y --append-only no deben tratarse como sustitutos de la actualización. Los registros de rutas anómalas y de operaciones contra objetos hermanos pueden ayudar en una investigación, aunque la ausencia de esos registros no prueba que la explotación nunca ocurrió.

Un Web Application Firewall puede rechazar patrones de ruta conocidos como medida de emergencia, pero las diferencias de normalización entre el proxy y la aplicación vuelven frágil una regla así. El control definitivo pertenece al componente que interpreta la ruta, y ya está disponible en la versión corregida.

Conclusión

La CVE-2026-71309 ilustra la diferencia crítica entre limpiar una ruta y validar una frontera. path.Clean producía una representación canónica pero preservaba los componentes superiores iniciales. La verificación de igualdad interpretaba esa estabilidad como seguridad. Una vez aceptado, el valor era considerado confiable por los handlers y los backends, y algunas implementaciones eliminaban la raíz publicada al unir rutas.

El resultado podía romper el aislamiento entre subcarpetas y permitir lecturas o modificaciones de objetos externos dentro del alcance de la credencial del backend. La corrección centraliza una regla de validez explícita con io/fs.ValidPath y cubre los métodos principales y las variantes de ruta con pruebas de regresión.

Los operadores deben actualizar a la versión 1.75.0 o posterior, verificar el binario que realmente se ejecuta y revisar si las credenciales de almacenamiento imponen la misma frontera que la API pretende publicar. El aislamiento de credenciales y el menor privilegio siguen siendo valiosos, pero complementan la corrección del software en lugar de reemplazarla.

Referencias técnicas