{"record":{"id":"0d68f30a14aa785f","repo":"ruvnet/ruflo","slug":"private-address","errorCode":"PRIVATE_ADDRESS","errorMessage":"host ${host} is loopback/private/link-local; set CLAUDE_FLOW_HTTP_FETCH_ALLOW_PRIVATE=1 to override","messagePattern":"host (.+?) is loopback/private/link-local; set CLAUDE_FLOW_HTTP_FETCH_ALLOW_PRIVATE=1 to override","errorType":"validation","errorClass":"HttpFetchValidationError","httpStatus":null,"severity":"error","filePath":"v3/@claude-flow/cli/src/mcp-tools/http-fetch-tools.ts","lineNumber":63,"sourceCode":" */\nexport function validateUrl(rawUrl: string): URL {\n  let parsed: URL;\n  try {\n    parsed = new URL(rawUrl);\n  } catch {\n    throw new HttpFetchValidationError(`invalid URL: ${rawUrl}`, 'INVALID_URL');\n  }\n  const proto = parsed.protocol.toLowerCase();\n  if (proto !== 'http:' && proto !== 'https:') {\n    throw new HttpFetchValidationError(\n      `protocol ${parsed.protocol} not allowed (only http: and https:)`,\n      'FORBIDDEN_PROTOCOL',\n    );\n  }\n  const host = parsed.hostname.toLowerCase();\n  const allowPrivate = process.env.CLAUDE_FLOW_HTTP_FETCH_ALLOW_PRIVATE === '1';\n  if (!allowPrivate && isPrivateOrLoopback(host)) {\n    throw new HttpFetchValidationError(\n      `host ${host} is loopback/private/link-local; set CLAUDE_FLOW_HTTP_FETCH_ALLOW_PRIVATE=1 to override`,\n      'PRIVATE_ADDRESS',\n    );\n  }\n  return parsed;\n}\n\nfunction isPrivateOrLoopback(host: string): boolean {\n  if (host === 'localhost' || host === 'localhost.localdomain') return true;\n  // IPv6 loopback\n  if (host === '::1' || host === '[::1]') return true;\n  // IPv4 numeric checks\n  const m = host.match(/^(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})$/);\n  if (m) {\n    const a = Number(m[1]);\n    const b = Number(m[2]);\n    if (a === 0) return true;          // 0.0.0.0/8\n    if (a === 127) return true;        // loopback","sourceCodeStart":45,"sourceCodeEnd":81,"githubUrl":"https://github.com/ruvnet/ruflo/blob/fa13ee4ad60ac2090b1480656eb233521790d640/v3/@claude-flow/cli/src/mcp-tools/http-fetch-tools.ts#L45-L81","documentation":"HttpFetchValidationError with code PRIVATE_ADDRESS, thrown by validateUrl() (http-fetch-tools.ts:63) when the URL's host is loopback ('localhost', '::1'), RFC-1918 private (10.x, 172.16-31.x, 192.168.x), link-local (169.254.x, fe80:), or IPv6 ULA (fc/fd prefix). This is an SSRF guard: an MCP-served fetch tool must not be redirectable at internal services, cloud metadata endpoints, or the user's own machine. It can be deliberately lifted with CLAUDE_FLOW_HTTP_FETCH_ALLOW_PRIVATE=1.","triggerScenarios":"http_fetch against 'http://localhost:3000/api', 'http://127.0.0.1:8080', 'http://192.168.1.10/admin', 'http://10.0.4.7/health', or a docker/k8s-internal service name that resolves to a private range — with CLAUDE_FLOW_HTTP_FETCH_ALLOW_PRIVATE unset or not '1'. DNS names resolving to private IPs are caught by the host check when the hostname itself is private; numeric checks run on the literal host.","commonSituations":"Developers testing against a local dev server; fetching an internal service in docker-compose or k8s; CI where the target is an internal endpoint; users surprised the tool will not reach their own laptop's server.","solutions":["If internal fetching is intended and the environment is trusted, set CLAUDE_FLOW_HTTP_FETCH_ALLOW_PRIVATE=1 in the MCP server's environment and restart it","Otherwise expose the target through a public endpoint or an authenticated tunnel","Keep the override scoped to the specific environment — do not bake it into shared images"],"exampleFix":"# before\n$ claude-flow mcp start\n# http_fetch { url: 'http://localhost:3000/api' }\n# -> PRIVATE_ADDRESS: host localhost is loopback/private/link-local\n\n# after (deliberate, trusted local dev)\n$ CLAUDE_FLOW_HTTP_FETCH_ALLOW_PRIVATE=1 claude-flow mcp start\n# http_fetch { url: 'http://localhost:3000/api' } -> 200","handlingStrategy":"validation","validationCode":"function isPrivateHost(host: string): boolean {\n  const h = host.toLowerCase();\n  if (h === 'localhost' || h === '::1' || h === '[::1]') return true;\n  if (/^10\\./.test(h) || /^192\\.168\\./.test(h)) return true;\n  if (/^172\\.(1[6-9]|2\\d|3[01])\\./.test(h)) return true;\n  if (/^169\\.254\\./.test(h)) return true;\n  if (h.startsWith('fc') || h.startsWith('fd') || h.startsWith('fe80:')) return true;\n  return false;\n}\nconst host = new URL(url).hostname;\nconst allowPrivate = process.env.CLAUDE_FLOW_HTTP_FETCH_ALLOW_PRIVATE === '1';\nif (isPrivateHost(host) && !allowPrivate) {\n  throw new Error('target is private/loopback; set CLAUDE_FLOW_HTTP_FETCH_ALLOW_PRIVATE=1 if intended');\n}","typeGuard":null,"tryCatchPattern":"try {\n  await callTool('http_fetch', { url });\n} catch (e) {\n  if (e instanceof HttpFetchValidationError && e.code === 'PRIVATE_ADDRESS') {\n    // deliberate local-dev fetch: retry with the override set in the server env, or fall back to your own fetch\n    return fetchLocal(url);\n  }\n  throw e;\n}","preventionTips":["For local/internal targets, launch the MCP server with CLAUDE_FLOW_HTTP_FETCH_ALLOW_PRIVATE=1 deliberately, never globally by default","Do not use the fetch tool to reach cloud metadata or internal admin surfaces — the block is the SSRF guard working","Keep the override scoped to dev environments only"],"tags":["security","ssrf","network","http","mcp"],"backgroundTag":"ssrf-private-address-blocked","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"}