{"record":{"id":"ac5b2de3fa3b5f45","repo":"ruvnet/ruflo","slug":"resolved-ip-for-hostname-is-internal-address","errorCode":null,"errorMessage":"Resolved IP for ${hostname} is internal (${address})","messagePattern":"Resolved IP for (.+?) is internal \\((.+?)\\)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"critical","filePath":"ruflo/src/ruvocal/src/lib/server/urlSafety.ts","lineNumber":75,"sourceCode":"\t\t// If the hostname is a raw IP literal, validate it\n\t\tconst cleanHostname = hostname.replace(/^\\[|]$/g, \"\");\n\t\tif (isIP(cleanHostname)) {\n\t\t\treturn !isUnsafeIp(cleanHostname);\n\t\t}\n\t\treturn true;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/**\n * Assert that a resolved IP address is safe (not internal/private).\n * Throws if the IP is internal. Used in undici's custom DNS lookup\n * to validate IPs at connection time (prevents TOCTOU DNS rebinding).\n */\nexport function assertSafeIp(address: string, hostname: string): void {\n\tif (isUnsafeIp(address)) {\n\t\tthrow new Error(`Resolved IP for ${hostname} is internal (${address})`);\n\t}\n}\n","sourceCodeStart":57,"sourceCodeEnd":78,"githubUrl":"https://github.com/ruvnet/ruflo/blob/6b01dc5a687b26b3e218f796de45ec51f8fa9e8c/ruflo/src/ruvocal/src/lib/server/urlSafety.ts#L57-L78","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Use a public hostname for the target so DNS returns a routable IP.","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.","For a hostname you control, point its DNS at a public IP instead of an RFC1918 address.","Confirm you are not the victim of DNS rebinding: resolve the hostname twice and compare, or pin the expected IP.","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."],"exampleFix":"// before\nawait fetch(userSuppliedUrl); // throws if DNS resolves to 127.0.0.1\n// after\nif (!isValidUrl(userSuppliedUrl)) throw new Error(\"disallowed URL\");\ntry {\n  await fetch(userSuppliedUrl); // assertSafeIp runs inside the DNS lookup\n} catch (e) {\n  if (String(e?.message ?? \"\").includes(\"is internal\")) throw new Error(\"blocked: internal target\");\n  throw e;\n}","handlingStrategy":"try-catch","validationCode":"import { isValidUrl } from \"$lib/server/urlSafety\";\n\nfunction isFetchableUrl(urlString: string): boolean {\n  return isValidUrl(urlString); // synchronous protocol+hostname check (pre-DNS)\n}\n\n// call before fetch:\nif (!isFetchableUrl(target)) throw new Error(`refusing to fetch ${target}`);","typeGuard":"function isPublicHostname(hostname: string): boolean {\n  const h = hostname.toLowerCase().replace(/^\\[|]$/g, \"\");\n  // note: this is the *string* check only; the real SSRF guard is assertSafeIp at connect time\n  return h !== \"localhost\" && h !== \"127.0.0.1\" && h !== \"::1\" && h.includes(\".\");\n}","tryCatchPattern":"import { assertSafeIp } from \"$lib/server/urlSafety\";\n\ntry {\n  await fetch(target); // assertSafeIp runs inside undici's DNS lookup\n} catch (e) {\n  const msg = String((e as Error)?.message ?? e);\n  if (msg.includes(\"is internal\")) {\n    // security policy: do not retry, do not fall back; log and reject\n    throw new Error(`blocked SSRF attempt: ${msg}`);\n  }\n  throw e; // genuine network error, caller may retry\n}","preventionTips":["Never disable the custom undici DNS lookup to work around this; widen a documented allowlist instead.","Resolve and log the target IP once before fetch in dev to catch internal targets early.","For dev-only internal targets, route them through a code path that does not use assertSafeIp rather than weakening the guard.","Watch for IPv4-mapped IPv6 (::ffff:127.0.0.1) in user-supplied URLs — the guard catches these, but pre-filtering is cheaper."],"tags":["security","ssrf","network","dns","server-side"],"backgroundTag":null,"analyzedSha":"6b01dc5a687b26b3e218f796de45ec51f8fa9e8c","analyzedAt":"2026-08-12T13:20:50.148Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}