{"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":"validation","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/fa13ee4ad60ac2090b1480656eb233521790d640/ruflo/src/ruvocal/mcp-bridge/index.js#L732-L768","documentation":"task_status first tries the swarm orchestrator when one is attached to the ToolContext, and on failure falls through to the 'simple implementation': an in-memory Map<string, Task> that is only populated by task_create in the same process. If taskStore.get(input.taskId) returns undefined, it throws 'Task not found: <id>'. So the error means the ID was never created here, came from another process, or the orchestrator lookup failed and the local store had no fallback copy.","triggerScenarios":"Calling task_status with an ID returned by task_create in a previous MCP server run; a typo'd/truncated taskId; the orchestrator call threw (you will see 'Failed to get task status via orchestrator:' on the server console) and the local Map never contained the task; asking for a task created by a different swarm coordinator instance.","commonSituations":"Server restarts between creating and polling tasks (the store is process-local with no persistence); multi-client setups where one client's restart orphans another's task IDs; IDs copied from logs with formatting artifacts.","solutions":["Confirm the taskId came from the task_create response in the same server process — re-create the task if the process restarted","List tasks (task list) and verify the exact ID before polling status","Check server console for 'Failed to get task status via orchestrator' — if present, the orchestrator path is broken; fix or restart the swarm coordinator so tasks are reachable there","Keep your own mapping of logical job name -> taskId so you never hand-type IDs"],"exampleFix":"// before\nawait client.callTool('task_status', { taskId: 'tsk-001' }); // hand-written id -> throws [1133]\n\n// after\nconst { task } = await client.callTool('task_create', { type: 'implementation', description: '...' });\nawait client.callTool('task_status', { taskId: task.id, includeMetrics: true });","handlingStrategy":"validation","validationCode":"const { tasks } = await client.callTool('task_list', { status: 'all' });\nconst taskExists = tasks.some(t => t.id === taskId);\nif (!taskExists) throw new Error(`task ${taskId} not in store — recreate before polling`);","typeGuard":"function isLiveTaskId(id: string, knownIds: Set<string>): boolean {\n  return knownIds.has(id);\n}","tryCatchPattern":"try {\n  await client.callTool('task_status', { taskId });\n} catch (e) {\n  if (e instanceof Error && e.message.startsWith('Task not found')) {\n    // store was reset (restart) or orchestrator fell through — recreate or stop polling\n    return null;\n  }\n  throw e;\n}","preventionTips":["Capture task.id from every task_create response into a durable registry","Reset your registry whenever the MCP server process restarts","Watch for 'Failed to ... via orchestrator' console lines — they signal the fallback path is in use"],"tags":["mcp","task","not-found","in-memory-state","orchestrator"],"backgroundTag":"task-not-found","analyzedSha":"fa13ee4ad60ac2090b1480656eb233521790d640","analyzedAt":"2026-08-18T21:34:22.708Z","contentChangedAt":"2026-08-18T21:34:22.708Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}