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
- Point the config at a public-facing HTTPS endpoint for the target service.
- Expose the internal service through a public ingress with auth, then use its https URL.
- Do not allow caller-supplied URLs to reach callCloudFunction without an allowlist of permitted hosts.
- 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
- Point configs at public HTTPS endpoints, not loopback/internal IPs.
- Allowlist permitted hosts before calling callCloudFunction.
- Remember the regex is incomplete — do not rely on it alone for untrusted input.
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
- SSRF guard: invalid URL — ${rawUrl}
- SSRF guard: only HTTPS URLs are permitted, got ${parsed.prot
- SSRF guard: invalid URL — ${rawUrl}
- SSRF guard: private/loopback host rejected — ${host}
- Resolved IP for ${hostname} is internal (${address})
AI-assisted analysis of ruvnet/ruflo@6b01dc5a68 (2026-08-12).
Data as JSON: /api/errors/b9e91fe32955b78e.
Report an issue: GitHub.