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

  1. Use the target service's public IP or public hostname
  2. Expose the private service through a public reverse proxy/tunnel and use that URL
  3. 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

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


AI-assisted analysis of decolua/9router@90b52e06ff (2026-08-30). Data as JSON: /api/errors/db9d4a0cb063076c. Report an issue: GitHub.