{"record":{"id":"3d9c4508bac100a1","repo":"ComposioHQ/composio","slug":"refusing-to-fetch-host-it-resolves-to-a-pri","errorCode":null,"errorMessage":"Refusing to fetch \"${host}\" — it resolves to a private, loopback, or link-local address","messagePattern":"Refusing to fetch \"(.+?)\" — it resolves to a private, loopback, or link-local address","errorType":"exception","errorClass":"ComposioBlockedInternalUrlError","httpStatus":null,"severity":"error","filePath":"ts/packages/core/src/utils/ssrfGuard.node.ts","lineNumber":201,"sourceCode":"    );\n  }\n\n  const host = url.hostname.replace(/^\\[|\\]$/g, '');\n\n  let resolved: Array<{ address: string }>;\n  try {\n    resolved = await lookup(host, { all: true, verbatim: true });\n  } catch {\n    throw new ComposioBlockedInternalUrlError(`Could not resolve host \"${host}\"`, { url: rawUrl });\n  }\n\n  if (resolved.length === 0) {\n    throw new ComposioBlockedInternalUrlError(`Could not resolve host \"${host}\"`, { url: rawUrl });\n  }\n\n  for (const { address } of resolved) {\n    if (isBlockedIp(address)) {\n      throw new ComposioBlockedInternalUrlError(\n        `Refusing to fetch \"${host}\" — it resolves to a private, loopback, or link-local address`,\n        { url: rawUrl, resolvedIp: address }\n      );\n    }\n  }\n\n  // Every answer was validated, so all of them are safe to connect to, and\n  // resolver order is the system's own address preference.\n  return resolved.map(({ address }) => address);\n};\n\n/**\n * Drop-in replacement for `fetch` that blocks SSRF. Validates the target, then\n * connects to the address it validated, and re-validates and re-pins every\n * redirect hop (redirects are followed manually up to {@link MAX_REDIRECTS}).\n * Intermediate redirect bodies are cancelled; non-redirect responses are\n * returned unchanged.\n *","sourceCodeStart":183,"sourceCodeEnd":219,"githubUrl":"https://github.com/ComposioHQ/composio/blob/64b1b85502b1beeb2379e6c9e8bf1104504fa637/ts/packages/core/src/utils/ssrfGuard.node.ts#L183-L219","documentation":"The core SSRF protection: after resolving the URL's hostname, every resolved IP is checked against private/loopback/link-local ranges (and similar). If any address is in a blocked range, the fetch is refused before connecting, with the offending resolvedIp attached.","triggerScenarios":"Passing a URL whose hostname resolves to 127.0.0.1, 10.x, 172.16-31.x, 192.168.x, 169.254.x, ::1, fc00::/7, etc. — including public-looking hostnames (nip.io, sslip.io, localtunnel) that map to internal IPs, and DNS rebinding attempts.","commonSituations":"Trying to upload from localhost ('http://localhost:3000/file') or an internal service; using wildcard-DNS services that resolve to private IPs; the SDK server running in a network where the public host resolves internally via split-horizon DNS.","solutions":["Host the file at a genuinely public URL and pass that","If the file is local to your app, pass the bytes/File directly instead of a URL","For split-horizon DNS, use the public resolution path (external DNS) or an externally reachable host"],"exampleFix":"// before\nawait upload.uploadFileAtUrl('http://localhost:3000/tmp/report.pdf');\n\n// after\nconst res = await fetch('http://localhost:3000/tmp/report.pdf');\nconst file = await res.blob();\nawait upload.uploadFile(file);","handlingStrategy":"validation","validationCode":"import { isIP } from 'node:net';\nimport { lookup } from 'node:dns/promises';\nconst addrs = await lookup(host, { all: true }).catch(() => []);\nconst isPrivate = (ip: string) =>\n  ip.startsWith('127.') || ip.startsWith('10.') || ip.startsWith('192.168.') ||\n  /^172\\.(1[6-9]|2\\d|3[01])\\./.test(ip) || ip === '::1' || ip.startsWith('fe80:') || ip.startsWith('fc');\nif (addrs.some(a => isPrivate(a.address))) throw new Error('URL targets private IP');","typeGuard":"null","tryCatchPattern":"try {\n  await upload.uploadFileAtUrl(url);\n} catch (e) {\n  if (e instanceof ComposioBlockedInternalUrlError && 'resolvedIp' in (e as any)) {\n    // host resolves internally; upload bytes directly instead\n  }\n}","preventionTips":["Never pass localhost/internal URLs to URL-upload APIs; upload bytes instead","Prefer uploading File/ArrayBuffer for anything sourced internally","Beware wildcard-DNS services and split-horizon DNS when choosing URLs"],"tags":["ssrf","security","network","private-ip","url-validation"],"backgroundTag":"ssrf-private-ip-blocked","analyzedSha":"64b1b85502b1beeb2379e6c9e8bf1104504fa637","analyzedAt":"2026-08-28T15:39:33.623Z","schemaVersion":2},"datasetVersion":"2026-08-28T16:17:29.566Z"}