ruvnet/ruflo · critical · Error

Resolved IP for ${hostname} is internal (${address})

Error message

Resolved IP for ${hostname} is internal (${address})

What it means

Thrown by assertSafeIp(), the SSRF defence wired into undici's custom DNS lookup so the resolved IP is checked at connect time (closing the TOCTOU window that pure hostname validation leaves open). isUnsafeIp() blocks 0.0.0.0/8, 100.64.0.0/10 (CGNAT), 127.0.0.0/8, 169.254.0.0/16, 172.16.0.0/12, 192.168.0.0/16, IPv6 loopback/link-local, IPv4-mapped IPv6 (::ffff:...), and any unparseable address. It is a hard security guard, not a configurable allowlist.

Source

Thrown at ruflo/src/ruvocal/src/lib/server/urlSafety.ts:75

		// If the hostname is a raw IP literal, validate it
		const cleanHostname = hostname.replace(/^\[|]$/g, "");
		if (isIP(cleanHostname)) {
			return !isUnsafeIp(cleanHostname);
		}
		return true;
	} catch {
		return false;
	}
}

/**
 * Assert that a resolved IP address is safe (not internal/private).
 * Throws if the IP is internal. Used in undici's custom DNS lookup
 * to validate IPs at connection time (prevents TOCTOU DNS rebinding).
 */
export function assertSafeIp(address: string, hostname: string): void {
	if (isUnsafeIp(address)) {
		throw new Error(`Resolved IP for ${hostname} is internal (${address})`);
	}
}

View on GitHub (pinned to 6b01dc5a68)

Solutions

  1. Use a public hostname for the target so DNS returns a routable IP.
  2. If a private target is genuinely required (dev/local MCP bridge), route that call through a code path that does NOT use the assertSafeIp lookup, or run with the dev posture that allows localhost/host.docker.internal.
  3. For a hostname you control, point its DNS at a public IP instead of an RFC1918 address.
  4. Confirm you are not the victim of DNS rebinding: resolve the hostname twice and compare, or pin the expected IP.
  5. Note the blocklist omits 10.0.0.0/8 and 224.0.0.0/4 — do not rely on this gap; validate your own trust boundary explicitly.

Example fix

// before
await fetch(userSuppliedUrl); // throws if DNS resolves to 127.0.0.1
// after
if (!isValidUrl(userSuppliedUrl)) throw new Error("disallowed URL");
try {
  await fetch(userSuppliedUrl); // assertSafeIp runs inside the DNS lookup
} catch (e) {
  if (String(e?.message ?? "").includes("is internal")) throw new Error("blocked: internal target");
  throw e;
}
Defensive patterns

Strategy: try-catch

Validate before calling

import { isValidUrl } from "$lib/server/urlSafety";

function isFetchableUrl(urlString: string): boolean {
  return isValidUrl(urlString); // synchronous protocol+hostname check (pre-DNS)
}

// call before fetch:
if (!isFetchableUrl(target)) throw new Error(`refusing to fetch ${target}`);

Type guard

function isPublicHostname(hostname: string): boolean {
  const h = hostname.toLowerCase().replace(/^\[|]$/g, "");
  // note: this is the *string* check only; the real SSRF guard is assertSafeIp at connect time
  return h !== "localhost" && h !== "127.0.0.1" && h !== "::1" && h.includes(".");
}

Try / catch

import { assertSafeIp } from "$lib/server/urlSafety";

try {
  await fetch(target); // assertSafeIp runs inside undici's DNS lookup
} catch (e) {
  const msg = String((e as Error)?.message ?? e);
  if (msg.includes("is internal")) {
    // security policy: do not retry, do not fall back; log and reject
    throw new Error(`blocked SSRF attempt: ${msg}`);
  }
  throw e; // genuine network error, caller may retry
}

Prevention

When it happens

Trigger: Any outbound fetch whose hostname DNS-resolves to a private/loopback/link-local address: a URL pointing at localhost/127.0.0.1 in a non-dev posture, an internal service name that resolves onto the 172.16/12 or 192.168/16 range, a DNS-rebinding attack where a public-looking hostname flips to 127.0.0.1 between the isValidUrl check and connect, or an IPv6 ::1/::ffff:127.0.0.1 literal.

Common situations: Self-hosted deployments fetching an internal MCP/LLM endpoint by internal IP; Docker setups where a service name resolves to a bridge-network address; a test that hardcodes http://127.0.0.1 against the production-style fetch path; legitimate link-local AWS metadata-style addresses (169.254.x) being fetched.

Related errors


AI-assisted analysis of ruvnet/ruflo@6b01dc5a68 (2026-08-12). Data as JSON: /api/errors/ac5b2de3fa3b5f45. Report an issue: GitHub.