decolua/9router · error · Error

Blocked URL: internal host

Error message

Blocked URL: internal host

What it means

assertPublicUrl is an SSRF guard for server-side fetches: before fetching a caller-supplied URL, it rejects hostnames explicitly listed as internal (localhost, ip6-localhost, ip6-loopback). Line 52 throws when the URL's hostname (lowercased) is in BLOCKED_HOSTNAMES. The guard exists so an API caller cannot make the server request itself or loopback services.

Source

Thrown at src/shared/utils/ssrfGuard.js:52

    const mask = bits === 0 ? 0 : (0xffffffff << (32 - bits)) >>> 0;
    return (ip & mask) === (base & mask);
  });
}

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. Point the URL at the public hostname of the target service instead of localhost
  2. If the target really must be loopback, perform the fetch outside this guarded API
  3. Use a fully-qualified public domain reachable from the server

Example fix

// before
await fetchViaProxy("http://localhost:8080/api");
// throws: Blocked URL: internal host

// after
await fetchViaProxy("https://api.example.com/api");
Defensive patterns

Strategy: validation

Validate before calling

const BLOCKED = new Set(["localhost", "ip6-localhost", "ip6-loopback"]);
function isInternalHost(rawUrl) {
  try { return BLOCKED.has(new URL(rawUrl).hostname.toLowerCase()); }
  catch { return true; }
}
if (isInternalHost(targetUrl)) console.warn("Refusing to send internal host through proxy");

Type guard

function isPublicHttpUrl(v) {
  if (typeof v !== "string") return false;
  try {
    const u = new URL(v);
    return (u.protocol === "http:" || u.protocol === "https:") && !BLOCKED.has(u.hostname.toLowerCase());
  } catch { return false; }
}

Try / catch

try {
  await proxyFetch(url);
} catch (e) {
  if (e.message === "Blocked URL: internal host") {
    throw new HttpError(400, "Target URL must be a public hostname, not localhost");
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling resolveBaseUrl / POST / handleFetch with a URL whose hostname is exactly `localhost`, `ip6-localhost`, or `ip6-loopback` (e.g. `http://localhost:8080/api`).

Common situations: Developer points the fetch-proxy at a locally running service during development; config/env still contains `localhost` from a local setup moved to a server; testing the proxy endpoint with a loopback URL from an HTTP client.

Related errors


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