ruvnet/ruflo · error · Error
SSRF guard: private/loopback host rejected — ${host}
Error message
SSRF guard: private/loopback host rejected — ${host} What it means
Identical private/loopback host check to error 6, in ruflo/src/ruvocal/mcp-bridge/index.js:750. assertSafeUrl tests the hostname against PRIVATE_IP_RE plus 'localhost' and '.local' and rejects matches. Same caveat as error 6: the prefix regex is not a complete SSRF defense (no decimal/octal/hex IP encoding, no 169.254, no DNS-rebinding protection).
Source
Thrown at ruflo/src/ruvocal/mcp-bridge/index.js:750
// =============================================================================
// 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
- Configure a public HTTPS endpoint for the target service.
- Expose internal services via a public ingress with authentication.
- Allowlist permitted hosts before invoking callCloudFunction rather than relying solely on the regex.
- If you harden the guard, patch both mcp-bridge copies (this file and index.js:661) — they are independent.
Example fix
// before
await callCloudFunction('https://localhost:5173/api', payload);
// after
await callCloudFunction('https://chat.example.com/api', 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('ruvocal backend must be a public 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('Ruvocal backend is private/loopback', { cause: e }); throw e; } Prevention
- Use public HTTPS endpoints for ruvocal backends.
- Patch both mcp-bridge copies when hardening the SSRF guard.
- Allowlist permitted hosts for user-controllable URLs.
When it happens
Trigger: A ruvocal caller passes a URL whose host is private/loopback: 'https://127.0.0.1', 'https://localhost', 'https://10.x', 'https://svc.local', 'https://[::1]'.
Common situations: Pointing OPENAI_BASE_URL or an MCP endpoint at a local emulator or internal mesh address; a user-controllable tool endpoint targeted at cloud metadata (169.254.169.254 — note this specific address is NOT caught by the regex and is a latent gap); dev configs referencing host.docker.internal.
Related errors
- SSRF guard: private/loopback host rejected — ${host}
- SSRF guard: invalid URL — ${rawUrl}
- SSRF guard: only HTTPS URLs are permitted, got ${parsed.prot
- Resolved IP for ${hostname} is internal (${address})
- PRIVATE_ADDRESS
AI-assisted analysis of ruvnet/ruflo@6b01dc5a68 (2026-08-12).
Data as JSON: /api/errors/418af34851200c5d.
Report an issue: GitHub.