{"record":{"id":"1ebf251b0af49b7f","repo":"ruvnet/ruflo","slug":"ssrf-guard-invalid-url-rawurl-1ebf25","errorCode":null,"errorMessage":"SSRF guard: invalid URL — ${rawUrl}","messagePattern":"SSRF guard: invalid URL — (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"ruflo/src/ruvocal/mcp-bridge/index.js","lineNumber":743,"sourceCode":"    };\n  } catch (err) {\n    if (err.name === \"AbortError\" || err.name === \"TimeoutError\") return { error: \"Search timed out\" };\n    return { error: err.message };\n  }\n}\n\n// =============================================================================\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();","sourceCodeStart":725,"sourceCodeEnd":761,"githubUrl":"https://github.com/ruvnet/ruflo/blob/6b01dc5a687b26b3e218f796de45ec51f8fa9e8c/ruflo/src/ruvocal/mcp-bridge/index.js#L725-L761","documentation":"Identical SSRF guard to error 4, duplicated in ruflo/src/ruvocal/mcp-bridge/index.js:743. assertSafeUrl() runs new URL(rawUrl) in try/catch and rethrows when the constructor rejects, catching non-parseable URL strings before scheme/host checks. Both copies exist because the ruvocal (chat UI) package vendors its own mcp-bridge rather than importing the shared one.","triggerScenarios":"A chat-UI/MCP code path calls its local assertSafeUrl/callCloudFunction with a malformed URL: empty string, undefined, 'undefined', a bare hostname without scheme, or a string with illegal characters.","commonSituations":"A ruvocal MCP_SERVERS or backend base-url env var unset or blank; a template-built URL that drops the scheme; config loaded from .env.local with a quoting error; user-supplied tool endpoint that is not a valid URL.","solutions":["Set the relevant ruvocal env var (e.g. OPENAI_BASE_URL or MCP backend URL) to a full 'https://...' value.","Validate the URL with new URL() at app startup so misconfig fails loud at boot.","When building URLs from user input, construct via URL/URLSearchParams and check .origin before passing to callCloudFunction.","Keep the two mcp-bridge copies in sync if you patch the guard — the duplication means a fix in one file does not protect the other."],"exampleFix":"// before\nconst target = process.env.RUVOCAL_CF_URL; // undefined\nawait callCloudFunction(target, payload);\n\n// after\nconst target = process.env.RUVOCAL_CF_URL;\nif (!target) throw new Error('RUVOCAL_CF_URL missing');\nnew URL(target);\nawait callCloudFunction(target, payload);","handlingStrategy":"validation","validationCode":"function safeUrl(raw: string): URL | null { try { return new URL(raw); } catch { return null; } }\nconst u = safeUrl(process.env.RUVOCAL_CF_URL ?? '');\nif (!u) throw new Error('RUVOCAL_CF_URL is not a valid URL');","typeGuard":"function isParseableUrl(raw: string): boolean { try { new URL(raw); return true; } catch { return false; } }","tryCatchPattern":"try { await callCloudFunction(url, payload); } catch (e) { if (e instanceof Error && e.message.startsWith('SSRF guard: invalid URL')) throw new Error(`Ruvocal backend URL invalid: ${url}`, { cause: e }); throw e; }","preventionTips":["Validate ruvocal backend URLs at app startup.","Keep this mcp-bridge copy in sync with the shared one when patching.","Ensure MCP_SERVERS and base URLs are set to full https:// values."],"tags":["security","ssrf","url-validation","mcp-bridge","ruvocal"],"backgroundTag":null,"analyzedSha":"6b01dc5a687b26b3e218f796de45ec51f8fa9e8c","analyzedAt":"2026-08-12T13:20:50.148Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}