{"record":{"id":"b9e91fe32955b78e","repo":"ruvnet/ruflo","slug":"ssrf-guard-private-loopback-host-rejected-hos","errorCode":null,"errorMessage":"SSRF guard: private/loopback host rejected — ${host}","messagePattern":"SSRF guard: private/loopback host rejected — (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"ruflo/src/mcp-bridge/index.js","lineNumber":661,"sourceCode":"// =============================================================================\n// SSRF GUARD — Reject requests to private/loopback ranges (CWE-918)\n// =============================================================================\n\nconst PRIVATE_IP_RE = /^(?:10\\.|172\\.(?:1[6-9]|2\\d|3[01])\\.|192\\.168\\.|127\\.|0\\.|::1|fc|fd)/i;\n\nfunction assertSafeUrl(rawUrl) {\n  let parsed;\n  try {\n    parsed = new URL(rawUrl);\n  } catch {\n    throw new Error(`SSRF guard: invalid URL — ${rawUrl}`);\n  }\n  if (parsed.protocol !== \"https:\") {\n    throw new Error(`SSRF guard: only HTTPS URLs are permitted, got ${parsed.protocol}`);\n  }\n  const host = parsed.hostname;\n  if (PRIVATE_IP_RE.test(host) || host === \"localhost\" || host.endsWith(\".local\")) {\n    throw new Error(`SSRF guard: private/loopback host rejected — ${host}`);\n  }\n}\n\n// =============================================================================\n// HELPER — Call a backend Cloud Function / API\n// =============================================================================\n\nasync function callCloudFunction(url, payload, timeoutMs = 25000) {\n  // Validate the URL before making any network request.\n  assertSafeUrl(url);\n  const controller = new AbortController();\n  const timer = setTimeout(() => controller.abort(), timeoutMs);\n  try {\n    const resp = await fetch(url, {\n      method: \"POST\",\n      headers: { \"Content-Type\": \"application/json\" },\n      body: JSON.stringify(payload),\n      signal: controller.signal,","sourceCodeStart":643,"sourceCodeEnd":679,"githubUrl":"https://github.com/ruvnet/ruflo/blob/6b01dc5a687b26b3e218f796de45ec51f8fa9e8c/ruflo/src/mcp-bridge/index.js#L643-L679","documentation":"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.","triggerScenarios":"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]'.","commonSituations":"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.","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."],"exampleFix":"// before\nawait callCloudFunction('https://localhost:8080/func', payload);\n\n// after\nawait callCloudFunction('https://api.example.com/func', payload);","handlingStrategy":"validation","validationCode":"const PRIVATE = /^(?:10\\.|172\\.(?:1[6-9]|2\\d|3[01])\\.|192\\.168\\.|127\\.|0\\.)/;\nfunction isPublicHost(raw: string): boolean {\n  try { const h = new URL(raw).hostname; return !PRIVATE.test(h) && h !== 'localhost' && !h.endsWith('.local'); }\n  catch { return false; }\n}\nif (!isPublicHost(url)) throw new Error('refusing private host');","typeGuard":"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; } }","tryCatchPattern":"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; }","preventionTips":["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."],"tags":["security","ssrf","network","mcp-bridge"],"backgroundTag":null,"analyzedSha":"6b01dc5a687b26b3e218f796de45ec51f8fa9e8c","analyzedAt":"2026-08-12T13:20:50.148Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}