ruvnet/ruflo · error · Error

SSRF guard: private/loopback host rejected — ${host}

Error message

SSRF guard: private/loopback host rejected — ${host}

What it means

Third check in assertSafeUrl(): after scheme validation, the hostname is tested against PRIVATE_IP_RE (10.x, 172.16-31.x, 192.168.x, 127.x, 0.x, ::1, fc*, fd*), the literal 'localhost', and any '.local' suffix. A match is rejected to block SSRF against internal/loopback addresses. Note the regex is string-prefix based and does not cover all encodings (decimal/octal/hex IP literals, 169.254 link-local, DNS rebinding), so treat it as defense-in-depth, not a complete SSRF solution.

Source

Thrown at ruflo/src/mcp-bridge/index.js:661

// =============================================================================
// SSRF GUARD — Reject requests to private/loopback ranges (CWE-918)
// =============================================================================

const PRIVATE_IP_RE = /^(?:10\.|172\.(?:1[6-9]|2\d|3[01])\.|192\.168\.|127\.|0\.|::1|fc|fd)/i;

function assertSafeUrl(rawUrl) {
  let parsed;
  try {
    parsed = new URL(rawUrl);
  } catch {
    throw new Error(`SSRF guard: invalid URL — ${rawUrl}`);
  }
  if (parsed.protocol !== "https:") {
    throw new Error(`SSRF guard: only HTTPS URLs are permitted, got ${parsed.protocol}`);
  }
  const host = parsed.hostname;
  if (PRIVATE_IP_RE.test(host) || host === "localhost" || host.endsWith(".local")) {
    throw new Error(`SSRF guard: private/loopback host rejected — ${host}`);
  }
}

// =============================================================================
// HELPER — Call a backend Cloud Function / API
// =============================================================================

async function callCloudFunction(url, payload, timeoutMs = 25000) {
  // Validate the URL before making any network request.
  assertSafeUrl(url);
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), timeoutMs);
  try {
    const resp = await fetch(url, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(payload),
      signal: controller.signal,

View on GitHub (pinned to 6b01dc5a68)

Solutions

  1. Point the config at a public-facing HTTPS endpoint for the target service.
  2. Expose the internal service through a public ingress with auth, then use its https URL.
  3. Do not allow caller-supplied URLs to reach callCloudFunction without an allowlist of permitted hosts.
  4. If local development genuinely needs loopback, run with a reviewed local-only profile rather than weakening the guard.

Example fix

// before
await callCloudFunction('https://localhost:8080/func', payload);

// after
await callCloudFunction('https://api.example.com/func', payload);
Defensive patterns

Strategy: validation

Validate before calling

const PRIVATE = /^(?:10\.|172\.(?:1[6-9]|2\d|3[01])\.|192\.168\.|127\.|0\.)/;
function isPublicHost(raw: string): boolean {
  try { const h = new URL(raw).hostname; return !PRIVATE.test(h) && h !== 'localhost' && !h.endsWith('.local'); }
  catch { return false; }
}
if (!isPublicHost(url)) throw new Error('refusing private host');

Type guard

function isPublicHttpsUrl(raw: string): boolean { try { const u = new URL(raw); return u.protocol === 'https:' && !PRIVATE.test(u.hostname) && u.hostname !== 'localhost' && !u.hostname.endsWith('.local'); } catch { return false; } }

Try / catch

try { await callCloudFunction(url, payload); } catch (e) { if (e instanceof Error && /private\/loopback host rejected/.test(e.message)) throw new Error('Backend host is private/loopback — use a public endpoint', { cause: e }); throw e; }

Prevention

When it happens

Trigger: Passing a URL whose hostname is a private/loopback address or name: 'https://127.0.0.1', 'https://localhost', 'https://10.0.0.5', 'https://192.168.1.1', 'https://myhost.local', 'https://[::1]'.

Common situations: Testing against a local cloud-function emulator; pointing the config at an internal service mesh address; a user-controllable URL field that an attacker targets at metadata endpoints; dev configs that reference host.docker.internal or a .local mDNS name.

Related errors


AI-assisted analysis of ruvnet/ruflo@6b01dc5a68 (2026-08-12). Data as JSON: /api/errors/b9e91fe32955b78e. Report an issue: GitHub.