79% frente al 51%: los neobancos independientes quedan atrás en seguridad de aplicaciones. Leer el informe

Análisis a fondo

Análisis a fondo

Seafile SeaDoc: RCE a través de un enlace de compartición público (CVE-2026-129167)

Un token anónimo de SeaDoc llega a todos los endpoints del convertidor, donde una ruta absoluta coloca un paquete Python ejecutable; corregido en 2.0.11/3.0.8.

Resumen

  • Una ruta absoluta en la importación de Confluence de SeaDoc permite al titular anónimo de un enlace de compartición .sdoc público escribir un paquete Python en la ruta de biblioteca del convertidor y ejecutarlo. Haiwen corrigió el servidor el 2 de noviembre de 2026.
  • SeaDoc 2.0.4 a 2.0.10 y 3.0.0 a 3.0.7 están afectados. La instancia debe tener un enlace de compartición .sdoc público o un wiki publicado.
  • El registro CVE lo califica como CVSS v4.0 9.2 Crítica. No se ha notificado explotación y no está en el catálogo Known Exploited Vulnerabilities (KEV) de CISA a fecha del 14 de noviembre de 2026.
  • Actualice a SeaDoc 2.0.11 o 3.0.8, o a una versión posterior. Consulte "¿Cómo lo corrijo?" para el commit del convertidor y el rodeo.

¿Estoy afectado?

Está expuesto cuando se cumplen todas estas condiciones:

  • El despliegue ejecuta SeaDoc 2.0.4 a 2.0.10, o 3.0.0 a 3.0.7.
  • La instancia tiene un enlace de compartición .sdoc público o un wiki publicado. El token anónimo que incrusta la página de compartición es la credencial que usa el ataque.
  • El convertidor SeaDoc es accesible a través del frontal de Seafile. El despliegue oficial con Docker Compose lo expone bajo /sdoc-server/converter/.
  • El despliegue se hizo antes del 2 de noviembre de 2026, cuando Haiwen indica que se corrigió el componente sdoc-server de código cerrado.

Un despliegue sin enlace de compartición .sdoc público y sin wiki publicado no expone la ruta del token anónimo descrita aquí. Un usuario autenticado que pueda obtener un token SeaDoc todavía puede llegar al convertidor.

El convertidor acepta cualquier token SeaDoc válido. Así que un token emitido para un documento llega a todos los endpoints del convertidor. Trate la condición previa como una condición de la ruta de ataque, no como un estado seguro por defecto.

Rama Versiones afectadas Versión corregida
SeaDoc 2.0 2.0.4 a 2.0.10 2.0.11
SeaDoc 3.0 3.0.0 a 3.0.7 3.0.8

Las imágenes actuales en el momento de escribir son 2.0.13 y 3.0.9. Ambas incluyen la corrección.

Compruebe la imagen en ejecución y las comparticiones públicas:

# Image tag of the running SeaDoc container
docker inspect --format '{{.Config.Image}}' <seadoc-container>

# Images present on the host
docker image ls | grep sdoc-server

# Public links, in the Seafile web interface
# Open each library, then Sharing, and review the links that are public

La etiqueta de la imagen es la comprobación de versión fiable para el despliegue oficial. El archivo compose puede fijar una etiqueta como 2.0-latest o 3.0-latest, que puede cambiar sin que cambie la versión del contenedor en ejecución.

¿Cómo lo corrijo?

Actualice SeaDoc a 2.0.11 o 3.0.8, o a las actuales 2.0.13 o 3.0.9, y recree los contenedores.

  1. Descargue la imagen corregida y actualice la etiqueta en el archivo compose.
  2. Recree los contenedores seadoc y sdoc-server. Descargar la imagen sin recrear el contenedor deja el proceso antiguo en ejecución.
  3. Confirme la nueva etiqueta con docker inspect.
  4. Revise los enlaces .sdoc públicos y los wikis publicados. Retire los que no sean necesarios.
  5. Si no puede actualizar, bloquee los endpoints del convertidor en el proxy inverso, o retire todos los enlaces .sdoc públicos y despublique todos los wikis. El rodeo elimina la ruta del token anónimo; no corrige el recorrido de rutas.

La corrección del convertidor de código abierto seadoc-converter es el commit cb43162e del 29 de octubre de 2026. Rechaza un filename que no sea una cadena, no termine en .html.zip, o contenga /, \ o un byte NUL. También rechaza una space_key vacía, . y ...

El componente sdoc-server es de código cerrado. Haiwen dijo al investigador por correo electrónico el 2 de noviembre de 2026 que SeaDoc 2.0 y 3.0 están corregidos. Ningún commit ni aviso público describe esa mitad de la corrección.

¿Cuál es la causa raíz?

El convertidor aceptaba cualquier JWT de SeaDoc tras comprobar solo su firma. Después usaba un nombre de archivo controlado por el atacante como ruta absoluta para extraer el ZIP y conservaba los archivos extraídos mientras una subida quedaba bloqueada.

SeaDoc tiene tres procesos: seahub para la autenticación y el renderizado de páginas, sdoc-server para la edición colaborativa y seadoc-converter para la conversión de documentos. Los tres comparten la clave HMAC JWT_PRIVATE_KEY. Cuando un visitante abre un enlace de compartición .sdoc público, seahub incrusta un JWT en la página para que funcione la vista previa anónima.

El check_auth_token() del convertidor comprueba la firma con config.SEADOC_PRIVATE_KEY y devuelve verdadero. Nunca comprueba el documento para el que se emitió el token ni el permiso que lleva:

def check_auth_token(req):
    auth = req.headers.get('Authorization', '').split()
    if not auth or auth[0].lower() != 'token' or len(auth) != 2:
        return False

    token = auth[1]
    if not token:
        return False

    private_key = config.SEADOC_PRIVATE_KEY
    try:
        jwt.decode(token, private_key, algorithms=['HS256'])
    except (jwt.ExpiredSignatureError, jwt.InvalidSignatureError) as e:
        logger.exception(e)
        return False

    return True

El token anónimo es de solo lectura, pero el convertidor ignora el campo permission. Por eso el token llega a /api/v1/confluence-to-wiki/, que toma un filename y construye una ruta con os.path.join:

    underscore_index = filename.rfind('_')
    if underscore_index != -1:
        space_key = filename[underscore_index + 1:-len('.html.zip')]
    elif 'Confluence-space-export-' in filename:
        wiki_name = filename[:-len('.html.zip')]
        space_key = filename[len('Confluence-space-export-'):-len('.html.zip')]
    else:
        wiki_name = filename[:-len('.html.zip')]
        space_key = wiki_name
    try:
        extract_dir = '/tmp/wiki'
        space_dir = os.path.join(extract_dir, space_key)
        zip_file_path = os.path.join(extract_dir, filename)
        if not os.path.exists(extract_dir):
            os.mkdir(extract_dir)
        # Deploy on two machines
        is_same_machine = True
        if not os.path.exists(space_dir):
            os.mkdir(space_dir)
            is_same_machine = False
            response = requests.get(download_url)
            with ZipFile(BytesIO(response.content), 'r') as zip_ref:
                zip_ref.extractall(space_dir)

Cuando el segundo argumento de os.path.join empieza por una barra, Python descarta el primer argumento. Una space_key como /usr/lib/python3.12/encodings/cp273 hace que space_dir y zip_file_path sean absolutos. El convertidor crea el directorio y descomprime allí el ZIP del atacante.

La limpieza se ejecuta en un bloque finally solo después de que process_zip_file() regrese. Esa función envía el resultado a upload_url sin tiempo de espera. Un destino de subida que acepta la conexión y nunca responde mantiene la petición bloqueada. Así que el bloque finally no se ejecuta, y los archivos plantados permanecen en disco.

¿Cómo se explota?

  1. El atacante abre un enlace de compartición .sdoc público y lee el valor seadocAccessToken en el código fuente de la página. El token es una credencial anónima válida para ese documento.
  2. El atacante ejecuta un servidor que devuelve un ZIP con un __init__.py, acepta una conexión y nunca responde, y devuelve un error HTTP 500 con charset=cp273 cuando se le pide.
  3. El atacante envía una petición a /sdoc-server/converter/api/v1/confluence-to-wiki/ con un filename absoluto como /usr/lib/python3.12/encodings/cp273aaaaaaaaa, un download_url que sirve el ZIP y un upload_url que se queda bloqueado. El convertidor extrae el ZIP en el paquete Python encodings.
  4. El atacante envía una petición a /sdoc-server/converter/api/v1/file-convert/ con un upload_url que devuelve 500 y charset=cp273. El convertidor registra el cuerpo de la respuesta, y Response.text lo decodifica con el charset declarado.
  5. CPython importa de forma perezosa encodings.cp273. FileFinder comprueba el paquete de directorio cp273/__init__.py antes que el módulo cp273.py, así que el archivo plantado se ejecuta dentro del proceso del convertidor.

El análisis público del investigador incluye las dos peticiones y el servidor del atacante. La primera petición queda bloqueada mientras los archivos están en disco; la segunda desencadena la importación. El comando de prueba del análisis escribió uid=0 (root) desde el contenedor del convertidor.

La ruta no necesita reinicio, porque la importación ocurre bajo demanda en el proceso en ejecución. La única condición previa es un enlace de compartición .sdoc público o un wiki publicado que proporcione un token anónimo.

¿Cómo se desarrolla el ataque?

Un token de compartición anónimo pasa la comprobación de firma. Un nombre de archivo absoluto escribe un paquete Python en la ruta de biblioteca del convertidor. Un error de charset hace que CPython lo importe y lo ejecute.

flowchart LR A["La página de compartición .sdoc pública
incluye un JWT anónimo"] --> B["El convertidor solo comprueba
la firma del JWT"] B --> C["confluence-to-wiki acepta
el token en todos los endpoints"] C --> D["El filename absoluto hace escribir
os.path.join fuera de /tmp/wiki"] D --> E["El ZIP se extrae en el
paquete Python encodings"] E --> F["La subida bloqueada impide
la limpieza finally"] F --> G["charset=cp273 hace que CPython
importe el paquete plantado"] G --> H["El código se ejecuta en el
proceso del convertidor"]

¿Qué puede hacer un atacante?

Un atacante no autenticado que tiene un token de compartición público puede ejecutar código en el proceso del convertidor SeaDoc, que el contenedor oficial ejecuta como root.

El impacto incluye:

  • Leer y modificar los documentos a los que llega el convertidor, incluida la clave HMAC compartida en su configuración.
  • Usar esa clave para falsificar tokens SeaDoc de otros documentos y servicios.
  • Llegar a la red y a los volúmenes montados a los que accede el contenedor del convertidor.
  • Usar el convertidor como punto de apoyo en el despliegue de Seafile.

Las condiciones forman parte del impacto:

  • La instancia debe exponer un enlace de compartición .sdoc público o un wiki publicado.
  • El convertidor debe ser accesible desde la red del atacante.
  • El despliegue debe ser una versión afectada de SeaDoc construida antes del 2 de noviembre de 2026.
  • El atacante no necesita cuenta ni interacción del usuario.

El investigador demostró uid=0 (root) en el laboratorio oficial de Docker Compose. El código se ejecuta en el contenedor del convertidor, no en el servidor de aplicaciones de Seafile por defecto. La capacidad de llegar al host depende de los montajes y de la red del contenedor.

¿Por qué funciona la corrección?

La corrección del convertidor rechaza cualquier nombre de archivo que pueda salir de /tmp/wiki, así que os.path.join ya no puede producir una ruta de extracción absoluta.

El commit de corrección cb43162e cambia la validación al inicio de confluence_to_wiki() y añade una comprobación después de derivar space_key:

-    if not filename:
+    if (not isinstance(filename, str)
+            or not filename.endswith('.html.zip')
+            or any(char in filename for char in ('/', '\\', '\x00'))):
         return {'error_msg': 'path invalid.'}, 400
...
     else:
         wiki_name = filename[:-len('.html.zip')]
         space_key = wiki_name
+
+    if space_key in ('', '.', '..'):
+        return {'error_msg': 'path invalid.'}, 400

     try:
         extract_dir = '/tmp/wiki'
         space_dir = os.path.join(extract_dir, space_key)

La comprobación rechaza una barra, una barra invertida y un byte NUL en filename, así que cada space_key derivada es un solo componente de ruta. La comprobación de space_key rechaza los tres valores que todavía apuntarían fuera de un hijo normal. La extracción permanece bajo /tmp/wiki.

Es una corrección parcial en el convertidor de código abierto. El cambio del sdoc-server de código cerrado que impide que un token anónimo de solo lectura llegue a todos los endpoints del convertidor no es público. Haiwen indica que el componente está corregido; el cambio de alcance del token no es visible para revisión.

¿Cómo compruebo si estuve comprometido?

No se han publicado indicadores de compromiso del proveedor. Las comprobaciones buscan las dos peticiones al convertidor y los archivos plantados en la ruta de biblioteca Python.

  • Busque en los registros del proxy inverso y de Seafile peticiones POST a /sdoc-server/converter/api/v1/confluence-to-wiki/ con un filename que empiece por / o contenga ...
  • Busque peticiones POST a /sdoc-server/converter/api/v1/file-convert/ cuyo upload_url apunte fuera de su infraestructura.
  • En el contenedor del convertidor, liste el directorio Python encodings en busca de paquetes que usted no instaló, sobre todo un directorio con el nombre de un códec como cp273.
  • Compruebe /tmp/wiki en busca de directorios y archivos ZIP sobrantes.
  • Compruebe el contenedor del convertidor en busca de archivos inesperados bajo /root, /tmp y el directorio de la aplicación.

La prueba de concepto del investigador escribe /root/pwned; un atacante puede escribir otra cosa. Una petición que falla antes de la extracción puede no dejar rastro, y el convertidor registra el cuerpo de la respuesta de subida, no la petición completa.

Cronología

  • 2026-10-24: el investigador publica el análisis.
  • 2026-10-29: se redacta el commit de corrección cb43162e del seadoc-converter.
  • 2026-11-02: Haiwen comunica al investigador que el componente sdoc-server de código cerrado está corregido.
  • 2026-11-13: se publica CVE-2026-129167 a las 17:21 UTC.

Del análisis al CVE en 20 días
Figura 1: el análisis se publicó el 24 de octubre de 2026 y la corrección del convertidor llegó el 29 de octubre. El proveedor corrigió el componente cerrado el 2 de noviembre y el CVE se publicó el 13 de noviembre. Fuente: análisis del investigador, commit de corrección y registro CVE, consultado el 14 de noviembre de 2026

Preguntas frecuentes

¿Se explota CVE-2026-129167 en la naturaleza?

No se ha notificado explotación. El CVE no está en el KEV de CISA a fecha del 14 de noviembre de 2026. El análisis del investigador describe una reproducción de laboratorio, no una campaña.

¿Necesito una cuenta para explotarlo?

No. El atacante necesita un enlace de compartición .sdoc público o un wiki publicado, y lee el token anónimo incrustado en la página de compartición. No hace falta una cuenta de Seafile.

¿El token anónimo limita el ataque a un solo documento?

No. El convertidor solo comprueba la firma del token. Los campos permission y de documento se ignoran, así que el token anónimo llega a todos los endpoints del convertidor.

¿Es vulnerable el servidor Seafile, o solo SeaDoc?

El fallo está en el convertidor SeaDoc y en la comprobación de token compartida por los servicios SeaDoc. Un despliegue de Seafile sin SeaDoc no está afectado por este CVE. El contenedor del convertidor es el proceso que ejecuta el código del atacante.

¿Qué versión lo corrige?

SeaDoc 2.0.11 y 3.0.8 son las primeras versiones corregidas. Las actuales 2.0.13 y 3.0.9 también incluyen la corrección. El componente sdoc-server de código cerrado se corrigió el 2 de noviembre de 2026.

¿La corrección es completa?

La corrección del convertidor de código abierto cierra el recorrido de ruta absoluta. El cambio de alcance del token en el sdoc-server de código cerrado no es público, así que su exhaustividad no puede revisarse a partir del código.

Referencias

  • Registro CVE-2026-129167, CNA VulnCheck, publicado el 2026-11-13, CVSS v4.0 9.2: https://www.cve.org/CVERecord?id=CVE-2026-129167 (consultado el 14 de noviembre de 2026)
  • Wu Yulun, "Seafile Vulnerability Report", publicado el 2026-10-24, la causa raíz, la reproducción y las dos peticiones: https://vul.yulun.ac.cn/20261024/seafile/ (consultado el 14 de noviembre de 2026)
  • Commit de corrección cb43162e del seadoc-converter, 29 de octubre de 2026, la validación del nombre de archivo y de space_key: https://github.com/haiwen/seadoc-converter/commit/cb43162e8bc2991a2900c8acf85090358a30e516 (consultado el 14 de noviembre de 2026)
  • Código fuente vulnerable del seadoc-converter en bb6cf0f, seadoc_converter/server/apis.py, check_auth_token() y confluence_to_wiki(): https://github.com/haiwen/seadoc-converter/blob/bb6cf0fb9ebc6bb05c9161acbfd6289737ec2cd6/seadoc_converter/server/apis.py (consultado el 14 de noviembre de 2026)
  • Código fuente vulnerable del seadoc-converter en bb6cf0f, seadoc_converter/converter/utils.py, la subida sin tiempo de espera: https://github.com/haiwen/seadoc-converter/blob/bb6cf0fb9ebc6bb05c9161acbfd6289737ec2cd6/seadoc_converter/converter/utils.py (consultado el 14 de noviembre de 2026)
  • Docker Hub, etiquetas de seafileltd/sdoc-server, 2.0.11, 3.0.8, 2.0.13 y 3.0.9: https://hub.docker.com/r/seafileltd/sdoc-server/tags (consultado el 14 de noviembre de 2026)
  • Docker Hub, recuento de descargas de seafileltd/seafile-mc, 20,336,673: https://hub.docker.com/r/seafileltd/seafile-mc (consultado el 14 de noviembre de 2026)
  • Catálogo Known Exploited Vulnerabilities de CISA, consultado para CVE-2026-129167 el 14 de noviembre de 2026 (no incluido): https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json (consultado el 14 de noviembre de 2026)