decolua/9router · warning

err.message (SSRF guard: blocked internal/private/metadata U

Error message

err.message (SSRF guard: blocked internal/private/metadata URL)

What it means

Before fetching anything, the handler runs assertPublicUrl(targetUrl) from src/shared/utils/ssrfGuard.js to block SSRF (Server-Side Request Forgery) targets: localhost, private/link-local IPs (10.x, 192.168.x, 169.254.x, ::1), cloud metadata endpoints (169.254.169.254), and other non-public addresses. When the guard throws, the error message itself is returned as an HTTP 400 body, surfacing as 'err.message (SSRF guard: blocked internal/private/metadata URL)'.

Source

Thrown at src/sse/handlers/fetch.js:86

  if (!targetUrl || typeof targetUrl !== "string") {
    log.warn("FETCH", "Missing url");
    return errorResponse(HTTP_STATUS.BAD_REQUEST, "Missing required field: url");
  }

  // Validate URL format
  try {
    new URL(targetUrl);
  } catch {
    log.warn("FETCH", "Invalid URL", { url: targetUrl });
    return errorResponse(HTTP_STATUS.BAD_REQUEST, "Invalid URL format");
  }

  // SSRF guard: reject internal/private/metadata targets
  try {
    assertPublicUrl(targetUrl);
  } catch (err) {
    log.warn("FETCH", "Blocked URL", { url: targetUrl });
    return errorResponse(HTTP_STATUS.BAD_REQUEST, err.message);
  }

  // Combo expansion: providerInput may be a combo name → run fallback/round-robin across providers
  const combos = await getCombos();
  const comboModels = getComboModelsFromData(providerInput, combos);
  if (comboModels) {
    const comboStrategies = settings.comboStrategies || {};
    const comboStrategy = comboStrategies[providerInput]?.fallbackStrategy || settings.comboStrategy || "fallback";
    const comboStickyLimit = settings.comboStickyRoundRobinLimit;
    log.info("FETCH", `Combo "${providerInput}" with ${comboModels.length} providers (strategy: ${comboStrategy}, sticky: ${comboStickyLimit})`);
    return handleComboChat({
      body,
      models: comboModels,
      handleSingleModel: (b, m) => handleSingleProviderFetch(b, m, request, apiKey, settings),
      log,
      comboName: providerInput,
      comboStrategy,

View on GitHub (pinned to 90b52e06ff)

Solutions

  1. Use a publicly routable http(s) URL - this guard is intentional and cannot be disabled per-request
  2. If you need internal pages, fetch them directly from your own code instead of routing through the gateway
  3. Check the returned err.message to see which class of address was blocked (loopback/private/metadata/scheme)
  4. If a legit public URL is blocked, verify DNS is not resolving it to a private IP (VPN/proxy/DNS override)

Example fix

// before
{ url: 'http://localhost:8080/api/page' }
// after
{ url: 'https://example.com/api/page' }
Defensive patterns

Strategy: validation

Validate before calling

function isPublicHttpUrl(value) {
  let u;
  try { u = new URL(value); } catch { return false; }
  if (u.protocol !== 'http:' && u.protocol !== 'https:') return false;
  const h = u.hostname;
  if (h === 'localhost' || h.endsWith('.localhost') || h.endsWith('.local') || h.endsWith('.internal')) return false;
  if (/^(10\.|127\.|169\.254\.|192\.168\.)/.test(h)) return false;
  if (/^172\.(1[6-9]|2\d|3[01])\./.test(h)) return false;
  if (h === '::1' || h.startsWith('fc') || h.startsWith('fd') || h.startsWith('fe80')) return false;
  return true;
}
if (!isPublicHttpUrl(url)) throw new Error('URL targets a private/loopback address - rejected by SSRF guard');

Type guard

function isPublicHttpUrl(value) {
  try { const u = new URL(value); return (u.protocol === 'http:' || u.protocol === 'https:') && !/^localhost$|^(127|10|169\.254|192\.168)\.|^172\.(1[6-9]|2\d|3[01])\.|^\[?::1\]?$/.test(u.hostname); } catch { return false; }
}

Try / catch

const res = await fetch(endpoint, { method: 'POST', body: JSON.stringify({ model, url }) });
if (res.status === 400) {
  const msg = await res.text();
  if (/ssrf|internal|private|metadata/i.test(msg)) {
    console.warn(`URL blocked by SSRF guard: ${url} (${msg})`); // do not retry - the block is intentional
    return;
  }
}

Prevention

When it happens

Trigger: Requesting http://localhost:3000, http://127.0.0.1, http://192.168.1.1, http://169.254.169.254/latest/meta-data, file:// or other non-http(s) schemes, or any URL resolving to a private/reserved range.

Common situations: Pointing the fetcher at a local dev server instead of a public site; internal tooling that legitimately wanted intranet pages (unsupported by design); redirect-based attacks where an attacker supplies a URL that hops to metadata; CI environments where outbound traffic goes through a proxy on a private IP.

Related errors


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