ruvnet/ruflo · error
Resolved IP for is internal ( )
Error message
Resolved IP for ${hostname} is internal (${address}) What it means
Thrown by assertSafeIp in urlSafety.ts, an SSRF defense hooked into undici's custom DNS resolution. After a hostname resolves, the IP is checked against internal ranges (IPv4 0.0.0.0/8, 100.64.0.0/10, 127/8, 169.254/16, 172.16/12, 192.168/16; IPv6 loopback, link-local, and IPv4-mapped forms like ::ffff:127.0.0.1; unknown formats are blocked too). Validating at connect time instead of URL-parse time closes the TOCTOU window of DNS rebinding.
Solutions
- Use a genuinely public URL — the block is intentional for user-supplied fetch targets.
- If you operate the deployment and genuinely need an internal target, whitelist it in the URL-safety layer (extend isValidUrl's explicit localhost/host.docker.internal allowance) instead of disabling assertSafeIp globally.
- For local development needs, prefer hostnames already allowed by isValidUrl (localhost, 127.0.0.1, host.docker.internal) rather than other RFC1918 addresses.
- Never remove the connect-time check in undici's lookup — that reopens DNS-rebinding SSRF.
Defensive patterns
Strategy: try-catch
Validate before calling
import { isValidUrl } from "$lib/server/urlSafety";
if (!isValidUrl(targetUrl)) {
return new Response("URL not allowed", { status: 400 });
} Type guard
import { isIP } from "node:net";
function isPublicIpLiteral(host: string): boolean {
const h = host.replace(/^\[|]$/g, "");
return isIP(h) === 0 ? true : !isInternalIp(h); // pair with your own subnet list
} Try / catch
try {
await fetchTarget(url);
} catch (err) {
if (String(err).includes("is internal")) {
return new Response("Refusing to fetch internal addresses", { status: 400 }); // expected block, not a bug
}
throw err;
} Prevention
- Validate user-supplied URLs with isValidUrl before any fetch.
- Keep the undici connect-time assertSafeIp hook — it is the DNS-rebinding defense; never bypass it.
- Expect and handle this error as a normal 400 path; do not widen the IP allowlist under pressure.
- Block unknown address formats (the guard already does) rather than defaulting to allow.
When it happens
Trigger: Any outbound fetch through the guarded client whose hostname resolves to a private/loopback/link-local IP: a URL pointing at http://127.0.0.1:8080 or http://192.168.1.10, an internal DNS name (e.g., http://metadata, http://db.internal) that resolves into RFC1918 space, or an external domain that rebinds to an internal address between validation and connection.
Common situations: Users asking the chat to fetch a URL on their LAN; a fetch-url endpoint being probed for cloud metadata (169.254.169.254); dev setups pointing at localhost services through a hostname that resolves internally; DNS records intentionally mapping a public name to an internal address.
Related errors
- PRIVATE_ADDRESS
- Invalid hostname
- network
- peer response exceeds size cap
- AI budget file is a symlink (refusing)
AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18).
Data as JSON: /api/errors/ac5b2de3fa3b5f45.
Report an issue: GitHub.
Appendix: 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 fa13ee4ad6)