decolua/9router · error · Error
Blocked URL: private IP
Error message
Blocked URL: private IP
What it means
assertPublicUrl line 54 fires when the hostname is a dotted IPv4 literal falling inside a private/reserved range (0.0.0.0/8, 10/8, 127/8, 169.254/16, 172.16/12, 192.168/16). The message is 'Blocked URL: private IP' — distinct from the hostname-based 'internal host' errors — signalling the target is a private network address rather than a public one.
Source
Thrown at src/shared/utils/ssrfGuard.js:54
});
}
function isBlockedIpv6(host) {
const h = host.replace(/^\[|\]$/g, "").toLowerCase();
const v4Mapped = h.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/);
if (v4Mapped) return isBlockedIpv4(v4Mapped[1]);
if (h === "::1" || h === "::") return true;
return h.startsWith("fe80:") || h.startsWith("fc") || h.startsWith("fd");
}
// Throw if URL targets a non-public host. Caller should map to 400.
export function assertPublicUrl(rawUrl) {
const parsed = new URL(rawUrl);
const host = parsed.hostname.toLowerCase();
if (BLOCKED_HOSTNAMES.has(host)) throw new Error("Blocked URL: internal host");
if (BLOCKED_SUFFIXES.some((s) => host.endsWith(s))) throw new Error("Blocked URL: internal host");
if (isBlockedIpv4(host)) throw new Error("Blocked URL: private IP");
if (host.includes(":") && isBlockedIpv6(host)) throw new Error("Blocked URL: private IP");
}
View on GitHub (pinned to 90b52e06ff)
Solutions
- Use the target service's public IP or public hostname
- Expose the private service through a public reverse proxy/tunnel and use that URL
- If this is a metadata-style probe in testing, remove it — the guard intentionally blocks it
Example fix
// before
await fetchViaProxy("http://192.168.1.10:3000/api");
// throws: Blocked URL: private IP
// after
await fetchViaProxy("https://my-service.example.com/api"); Defensive patterns
Strategy: validation
Validate before calling
function isPrivateIpv4(host) {
const m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(host);
if (!m) return false;
const [a, b] = m.slice(1).map(Number);
return a === 0 || a === 10 || a === 127 || (a === 169 && b === 254) || (a === 172 && b >= 16 && b <= 31) || (a === 192 && b === 168);
}
const host = new URL(rawUrl).hostname;
if (isPrivateIpv4(host)) console.warn("Blocked: private IPv4 target", host); Type guard
function isPublicIpv4Url(v) {
if (typeof v !== "string") return false;
try {
const host = new URL(v).hostname;
return /^\d{1,3}(\.\d{1,3}){3}$/.test(host) ? !isPrivateIpv4(host) : true;
} catch { return false; }
} Try / catch
try {
await proxyFetch(url);
} catch (e) {
if (e.message === "Blocked URL: private IP") {
throw new HttpError(400, "Target resolves to a private/reserved IPv4 address");
}
throw e;
} Prevention
- Never target 169.254.169.254 or other metadata IPs through the proxy — this is exactly what the guard blocks
- Use public hostnames instead of LAN IPs in any URL passed to server-side fetch endpoints
- For Docker/LAN dev services, expose them via a tunnel with a public URL if proxying is required
- Validate the target IP range client-side before submitting
When it happens
Trigger: Calling resolveBaseUrl / POST / handleFetch with a raw IPv4 literal in a reserved range, e.g. `http://10.0.0.5`, `http://192.168.1.1`, `http://172.16.0.1`, `http://127.0.0.1`, or a link-local `169.254.x.x` cloud-metadata address.
Common situations: Pointing the proxy at a home/office LAN device; testing against a local dev server bound to 127.0.0.1; accidentally using the cloud metadata IP 169.254.169.254 (a classic SSRF payload); Docker-internal 172.x addresses.
Related errors
- Blocked URL: internal host
- err.message (SSRF guard: blocked internal/private/metadata U
- Cursor AgentService request failed: ${error.message}
- AWS EventStream frame bounds are invalid
- Failed to fetch image: ${res.status}
AI-assisted analysis of decolua/9router@90b52e06ff (2026-08-30).
Data as JSON: /api/errors/db9d4a0cb063076c.
Report an issue: GitHub.