koala73/worldmonitor · error · McpProxySsrfError
serverUrl host is not allowed
Error message
serverUrl host is not allowed
What it means
Public-facing message thrown by throwBlockedAddress in api/mcp-proxy.ts when the resolved address of a serverUrl is flagged by isBlockedResolvedAddress (private/reserved ranges). The concrete blocked IP is logged server-side via console.error (event: mcp_proxy_ssrf_blocked) but never echoed to the caller — the caller only sees this generic string so the proxy cannot be used as an internal-address oracle. Thrown as a McpProxySsrfError.
Source
Thrown at api/mcp-proxy.ts:117
}
// Generic message surfaced to the caller when a serverUrl resolves to a
// private/reserved address. The specific blocked IP is deliberately NOT echoed
// back: returning it turns the proxy into an address oracle (the caller could
// enumerate internal IPs by observing which hostnames get blocked). SSRF review
// finding — log the concrete IP server-side for debugging, tell the caller only
// that the host is disallowed.
const SSRF_BLOCKED_PUBLIC_MESSAGE = 'serverUrl host is not allowed';
function throwBlockedAddress(blockedAddress) {
// Server-side audit/debug log with the concrete blocked address. This is the
// only place the resolved internal IP appears; it never reaches the response.
console.error('[mcp-proxy]', {
event: 'mcp_proxy_ssrf_blocked',
ts: new Date().toISOString(),
blocked_address: blockedAddress,
});
throw new McpProxySsrfError(SSRF_BLOCKED_PUBLIC_MESSAGE);
}
async function resolveDnsJson(hostname, recordType) {
const url = new URL(DNS_JSON_ENDPOINT);
url.searchParams.set('name', hostname);
url.searchParams.set('type', recordType);
const response = await fetch(url.toString(), {
headers: {
Accept: 'application/dns-json',
'User-Agent': 'WorldMonitor-MCP-Proxy/1.0',
},
signal: AbortSignal.timeout(DNS_RESOLUTION_TIMEOUT_MS),
});
if (!response.ok) {
throw new Error(`DNS ${recordType} lookup failed: HTTP ${response.status}`);
}
const data = await response.json();
if (data?.Status !== 0) {View on GitHub (pinned to ffec79ac33)
Solutions
- Point serverUrl at a public https MCP server whose DNS resolves to a public IP.
- Check the server's DNS for stray private-address records and remove them.
- Inspect server-side logs (event mcp_proxy_ssrf_blocked) for the concrete blocked address if you own the deployment.
- Do not attempt to bypass — the generic message is intentional; the residual DNS-rebind window is a tracked Edge limitation (issue #5061).
Example fix
// before
proxy({ serverUrl: 'https://internal.mcp.local' }) // resolves to 10.0.0.5
// after
proxy({ serverUrl: 'https://mcp.example.com' }) // resolves to a public IP Defensive patterns
Strategy: validation
Validate before calling
import { isBlockedResolvedAddress } from '../server/_shared/ip-address-classification';
async function serverUrlResolvesToPublic(url: string): Promise<boolean> {
const u = new URL(url);
const records = await Promise.all([
resolveDnsJson(u.hostname, 'A'), resolveDnsJson(u.hostname, 'AAAA'),
]);
const addrs = records.flat();
return addrs.length > 0 && !addrs.some(isBlockedResolvedAddress);
} Try / catch
try {
await assertServerUrlSafe(new URL(serverUrl));
} catch (err) {
if (err.name === 'McpProxySsrfError' && err.message === 'serverUrl host is not allowed') {
return res.status(400).json({ error: 'The MCP server host resolves to a blocked address.' });
}
throw err;
} Prevention
- Always point the MCP proxy at a public https server whose DNS resolves to a public IP.
- Audit your MCP server's DNS for stray private-address records.
- Remember the Edge runtime cannot pin the connection — DNS can rebind between validate and fetch (tracked in #5061).
When it happens
Trigger: A POST to /api/mcp-proxy whose `serverUrl` resolves (via DoH A/AAAA) to a private/reserved IP, or whose hostname is itself a blocked IP literal. assertServerUrlSafe calls throwBlockedAddress, which throws McpProxySsrfError with this message.
Common situations: A user (or attacker) points the MCP proxy at an internal address to attempt SSRF; a misconfigured MCP server URL that resolves internally; DNS-rebinding where the hostname flips to a private IP between validation and fetch.
Related errors
- serverUrl hostname is blocked: ${hostname}
- Webhook URL must not point to a private/local address
- serverUrl DNS resolution failed: ${message}
- serverUrl DNS resolution returned no addresses
- Webhook URL must not point to a metadata endpoint
AI-assisted analysis of koala73/worldmonitor@ffec79ac33 (2026-08-12).
Data as JSON: /api/errors/8cfa264a5d740aca.
Report an issue: GitHub.