{"record":{"id":"418af34851200c5d","repo":"ruvnet/ruflo","slug":"ssrf-guard-private-loopback-host-rejected-hos-418af3","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/ruvocal/mcp-bridge/index.js","lineNumber":750,"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":732,"sourceCodeEnd":768,"githubUrl":"https://github.com/ruvnet/ruflo/blob/6b01dc5a687b26b3e218f796de45ec51f8fa9e8c/ruflo/src/ruvocal/mcp-bridge/index.js#L732-L768","documentation":"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).","triggerScenarios":"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]'.","commonSituations":"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.","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."],"exampleFix":"// before\nawait callCloudFunction('https://localhost:5173/api', payload);\n\n// after\nawait callCloudFunction('https://chat.example.com/api', 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('ruvocal backend must be a public 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('Ruvocal backend is private/loopback', { cause: e }); throw e; }","preventionTips":["Use public HTTPS endpoints for ruvocal backends.","Patch both mcp-bridge copies when hardening the SSRF guard.","Allowlist permitted hosts for user-controllable URLs."],"tags":["security","ssrf","network","ruvocal"],"backgroundTag":null,"analyzedSha":"6b01dc5a687b26b3e218f796de45ec51f8fa9e8c","analyzedAt":"2026-08-12T13:20:50.148Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}