Resumen
Un servidor MCP contenerizado que se ejecuta con el perfil de permisos de red por defecto (insecure_allow_all: true) puede alcanzar servicios locales del host a traves de host.docker.internal. Esto incluye la propia API de ToolHive, otros proxies de servidores MCP gestionados por ToolHive y cualquier otro servicio que escuche en el localhost del host. Combinado con los endpoints de API de ToolHive y de proxy MCP sin autenticacion, esto permite que un servidor MCP comprometido o malicioso realice movimiento lateral sin necesidad de escapar del contenedor.
Severidad
Alta. Esta vulnerabilidad elude el modelo de aislamiento de contenedores que constituye la propuesta de valor de seguridad central de ToolHive.
Reproduccion
Todas las pruebas se realizaron desde dentro del contenedor MCP filesystem (docker.io/mcp/filesystem:latest), iniciado con la configuracion por defecto mediante thv run filesystem -- /tmp.
1. El contenedor puede alcanzar el endpoint MCP del plano de control de ToolHive
$ docker exec <container_id> wget -qO- \
--header="Content-Type: application/json" \
--header="Accept: application/json" \
--post-data='{"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"evil-mcp","version":"1.0"}},"id":1}' \
http://host.docker.internal:50444/mcp
Resultado: el handshake MCP completo tiene exito:
{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-03-26","capabilities":{"logging":{},"tools":{}},"serverInfo":{"name":"toolhive-mcp","version":"v0.9.3"}}}
2. El contenedor puede conectarse al proxy de otro servidor MCP y llamar a sus herramientas
$ docker exec <container_id> wget -qO- \
--header="Content-Type: application/json" \
--header="Accept: application/json" \
--post-data='{"jsonrpc":"2.0","method":"tools/list","params":{},"id":2}' \
http://host.docker.internal:64965/mcp
Resultado: devuelve la lista completa de herramientas del servidor MCP objetivo (read_file, write_file, edit_file, move_file, etc.) y las herramientas pueden ser invocadas:
{"jsonrpc":"2.0","id":3,"result":{"content":[{"text":"Allowed directories:\n/tmp","type":"text"}]}}
3. El contenedor puede alcanzar otros servicios del host
# API de Kubernetes
$ docker exec <container_id> wget -qO- --no-check-certificate https://host.docker.internal:6443/version
{"major":"1","minor":"34","gitVersion":"v1.34.1"...}
# API LLM de Ollama
$ docker exec <container_id> wget -qO- http://host.docker.internal:11434/api/tags
{"models":[{"name":"kimi-k2:1t-cloud"...}]}
Escenarios de Ataque
Escenario 1: Servidor MCP malicioso pivota hacia herramientas MCP nativas privilegiadas
Muchos usuarios ejecutan servidores MCP nativos (no contenerizados) como Desktop Commander, servidores de terminal o herramientas personalizadas que disponen de capacidades execute_command, write_file o shell con acceso completo al host. Estos tipicamente escuchan en puertos localhost. Un servidor MCP contenerizado malicioso puede:
- Escanear puertos en
host.docker.internalpara descubrir servicios en escucha. - Intentar handshakes MCP en los puertos descubiertos.
- Invocar herramientas privilegiadas (por ejemplo,
execute_command("rm -rf /")owrite_file("/etc/crontab", "...")).
Esto logra un compromiso total del host sin necesidad de ninguna vulnerabilidad de escape de contenedor.
Escenario 2: Servidor MCP comprometido manipula ToolHive
A traves del endpoint MCP de ToolHive sin autenticacion en el puerto 50444, un contenedor comprometido podria potencialmente:
- Listar y detener otros servidores MCP en ejecucion (DoS).
- Iniciar nuevos servidores MCP con imagenes controladas por el atacante.
- Modificar configuraciones.
Escenario 3: Exfiltracion de datos mediante acceso cruzado entre servidores MCP
Un servidor MCP con bajos privilegios (por ejemplo, sequentialthinking sin montajes de archivos) podria alcanzar el proxy del servidor filesystem e invocar read_file para acceder a archivos para los que nunca fue autorizado.
Escenario 4: Robo o abuso de modelos LLM
Tal como se demostro, el contenedor puede alcanzar la API de Ollama y podria enumerar modelos, ejecutar inferencia o exfiltrar pesos de modelos de LLMs autoalojados.
Causas Raiz
insecure_allow_all: truecomo valor por defecto: permite conexiones salientes hacia cualquier destino, incluyendohost.docker.internal.- Sin autenticacion en la API de ToolHive ni en los proxies MCP: cualquier cliente que pueda alcanzar el puerto puede interactuar completamente.
- DNS
host.docker.internalde Docker: resuelve hacia la maquina host, eludiendo las suposiciones de vinculacion exclusiva a localhost.
Mitigaciones Sugeridas
Corto plazo
- Bloquear
host.docker.internaly172.17.0.1(gateway de Docker) en la red de contenedores por defecto, incluso cuandoinsecure_allow_alleste habilitado. Estos destinos deberian requerir habilitacion explicita. - Agregar autenticacion a los endpoints de proxy MCP: incluso un secreto compartido o token por sesion prevendra el movimiento lateral entre contenedores.
Medio plazo
- Politica de red por contenedor: ToolHive ya dispone de la infraestructura
permission_profile. Agregar soporte para listas de permitidos explicitas en lugar de la eleccion binaria ninguno/todo. - Aislar redes de contenedores: ejecutar cada servidor MCP en su propia red Docker sin acceso al gateway del bridge de Docker.
Largo plazo
- TLS mutuo o autenticacion basada en tokens entre el proxy de ToolHive y los contenedores, de modo que incluso si existe acceso de red, las llamadas MCP no autorizadas sean rechazadas.
- Registro de auditoria: registrar todas las llamadas a herramientas MCP con identificacion de origen para que los intentos de movimiento lateral sean visibles.
Entorno Afectado
- ToolHive v0.9.3 (aplicacion de escritorio macOS, runtime Docker)
- Docker Desktop para Mac (host.docker.internal habilitado por defecto)
- Probado con
docker.io/mcp/filesystem:latest
