koala73/worldmonitor · error · McpProxySsrfError

serverUrl hostname is blocked: ${hostname}

Error message

serverUrl hostname is blocked: ${hostname}

What it means

Thrown by assertServerUrlSafe when the lowercased serverUrl hostname is in BLOCKED_HOSTNAMES (localhost, metadata, metadata.internal, metadata.google.internal, instance-data, computemetadata, link-local.s3.amazonaws.com, 169.254.169.254). This is the static cloud-metadata/localhost SSRF gate that runs before any DNS resolution. The hostname is interpolated into the message because it is already non-secret caller input.

Source

Thrown at api/mcp-proxy.ts:157

  return (Array.isArray(data?.Answer) ? data.Answer : [])
    .filter(answer => answer?.type === expectedType && typeof answer?.data === 'string')
    .map(answer => answer.data);
}

async function defaultResolveHostname(hostname) {
  const resolveHostnameForTest = getResolveHostnameForTest();
  if (resolveHostnameForTest) return resolveHostnameForTest(hostname);
  const records = await Promise.all([
    resolveDnsJson(hostname, 'A'),
    resolveDnsJson(hostname, 'AAAA'),
  ]);
  return records.flat();
}

async function assertServerUrlSafe(url) {
  const hostname = url.hostname.toLowerCase();
  if (BLOCKED_HOSTNAMES.has(hostname)) {
    throw new McpProxySsrfError(`serverUrl hostname is blocked: ${hostname}`);
  }
  if (isBlockedResolvedAddress(hostname)) {
    throwBlockedAddress(hostname);
  }

  let resolvedAddresses;
  try {
    resolvedAddresses = await defaultResolveHostname(hostname);
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error);
    throw new McpProxySsrfError(`serverUrl DNS resolution failed: ${message}`);
  }

  if (!resolvedAddresses.length) {
    throw new McpProxySsrfError('serverUrl DNS resolution returned no addresses');
  }

  const blocked = resolvedAddresses.find(isBlockedResolvedAddress);

View on GitHub (pinned to ffec79ac33)

Solutions

  1. Use a public https MCP server URL.
  2. For local dev, expose the local MCP server via an https tunnel before pointing the proxy at it.
  3. Remove any metadata/localhost hostnames from MCP config fixtures.

Example fix

// before
proxy({ serverUrl: 'https://localhost:8080/mcp' })
// after
proxy({ serverUrl: 'https://mcp-tunnel.example.dev/mcp' })
Defensive patterns

Strategy: validation

Validate before calling

const BLOCKED_HOSTNAMES = new Set([
  'localhost','metadata','metadata.internal','metadata.google.internal',
  'instance-data','computemetadata','link-local.s3.amazonaws.com','169.254.169.254',
]);

function serverUrlHostBlocked(rawUrl: string): boolean {
  try { return BLOCKED_HOSTNAMES.has(new URL(rawUrl).hostname.toLowerCase()); }
  catch { return false; }
}

Type guard

function isAllowedMcpHostname(value: unknown): boolean {
  if (typeof value !== 'string') return false;
  try { return !BLOCKED_HOSTNAMES.has(new URL(value).hostname.toLowerCase()); } catch { return false; }
}

Try / catch

try {
  await assertServerUrlSafe(new URL(serverUrl));
} catch (err) {
  if (err.name === 'McpProxySsrfError' && err.message.startsWith('serverUrl hostname is blocked:')) {
    return res.status(400).json({ error: 'That MCP server hostname is blocked.' });
  }
  throw err;
}

Prevention

When it happens

Trigger: POST /api/mcp-proxy with a serverUrl whose host is one of the blocked metadata/localhost literals, e.g. `https://169.254.169.254/`, `https://metadata.google.internal/`, or `https://localhost:8080/`.

Common situations: Local dev pointing the MCP proxy at localhost; an adversarial SSRF probe against cloud metadata; a stale config pointing at a metadata hostname.

Related errors


AI-assisted analysis of koala73/worldmonitor@ffec79ac33 (2026-08-12). Data as JSON: /api/errors/6bb817e301945a8d. Report an issue: GitHub.