{"record":{"id":"0983afde2918d309","repo":"santifer/career-ops","slug":"plugin-egress-hostname-resolves-to-a-blocked-a","errorCode":null,"errorMessage":"plugin egress: ${hostname} resolves to a blocked address (${address}) — possible SSRF/rebinding","messagePattern":"plugin egress: (.+?) resolves to a blocked address \\((.+?)\\) — possible SSRF/rebinding","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"plugins/_net.mjs","lineNumber":101,"sourceCode":"    return [hostname];\n  }\n\n  if (allowsLocalhost && LOOPBACK_HOSTS.has(hostname.toLowerCase())) {\n    // Local-AI providers (Ollama/LM Studio). Resolve but allow loopback through.\n    return ['127.0.0.1'];\n  }\n\n  let addrs;\n  try {\n    addrs = await dnsLookup(hostname, { all: true });\n  } catch (err) {\n    throw new Error(`plugin egress: cannot resolve ${hostname} — ${err.message}`);\n  }\n  if (!addrs.length) throw new Error(`plugin egress: ${hostname} resolved to no addresses`);\n  for (const { address } of addrs) {\n    if (isBlockedIp(address)) {\n      if (allowsLocalhost && isLoopbackLiteral(address)) continue;\n      throw new Error(`plugin egress: ${hostname} resolves to a blocked address (${address}) — possible SSRF/rebinding`);\n    }\n  }\n  return addrs.map(a => a.address);\n}\n\nfunction isLoopbackLiteral(ip) {\n  if (ip === '::1') return true;\n  if (isIP(ip) === 4) return ip.split('.')[0] === '127';\n  return false;\n}\n","sourceCodeStart":83,"sourceCodeEnd":112,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/plugins/_net.mjs#L83-L112","documentation":"After resolving the hostname, the egress guard checks every returned address against an SSRF blocklist (private/link-local/loopback ranges). If any resolved IP is blocked — and the loopback exemption (allowsLocalhost) does not apply — the request is aborted with this error rather than being sent to a protected address. This defends against SSRF and DNS-rebinding attacks where a public-looking hostname resolves to an internal IP.","triggerScenarios":"Any plugin network call whose hostname resolves to a blocked IP: hostnames pointing at 127.x, 10.x, 172.16-31.x, 192.168.x, link-local 169.254.x, or metadata endpoints like 169.254.169.254; also DNS-rebinding names that alternate between public and private answers.","commonSituations":"Pointing a plugin at an internal service by DNS name while local network access is disallowed; testing against 'localhost' or 'host.docker.internal' without the localhost allowance; a misconfigured internal DNS zone; using a hostname that rebinding-rotates to 127.0.0.1.","solutions":["Determine the resolved IP (`dig <host> +short`) and confirm whether it should be reachable; if it is a private/internal address by design, run with the localhost/internal allowance enabled (the allowsLocalhost path exempts loopback literals).","If the hostname is wrong, fix the config to use the public endpoint (e.g. the real external API host instead of an internal one).","Expose the internal service through an approved public gateway/proxy instead of addressing it by internal DNS.","If you are being hit by rebinding-style records (mixed public/private answers), pin the host to a vetted static address in /etc/hosts under your control."],"exampleFix":"// before (resolves to 10.0.0.4 → blocked)\nawait pluginFetch('https://internal.corp/api');\n// after: enable the local/internal allowance or use the public endpoint\nawait pluginFetch('https://api.public.example.com/api');","handlingStrategy":"validation","validationCode":"const dns = require('dns').promises;\nconst net = require('net');\nfunction isPrivate(ip) {\n  if (net.isIP(ip) === 0) return true;\n  if (ip.startsWith('127.') || ip.startsWith('10.') || ip.startsWith('192.168.') ||\n      ip.startsWith('169.254.')) return true;\n  const m = ip.match(/^172\\.(1[6-9]|2\\d|3[01])\\./);\n  return !!m;\n}\nasync function hostIsPublic(host) {\n  const addrs = await dns.lookup(host, { all: true });\n  return addrs.length > 0 && addrs.every(a => !isPrivate(a.address));\n}\n// call hostIsPublic(host) before the request","typeGuard":"function isLoopbackLiteralStr(ip) {\n  return typeof ip === 'string' && /^127\\./.test(ip);\n}","tryCatchPattern":"try {\n  return await pluginFetch(url);\n} catch (err) {\n  if (String(err.message).includes('blocked address')) {\n    throw new Error(`EGRESS_BLOCKED: ${url} targets a private/metadata IP; use the public endpoint or enable the localhost allowance`);\n  }\n  throw err;\n}","preventionTips":["Never point plugin egress at internal, loopback, or cloud-metadata hostnames.","Enable the localhost allowance only when you intentionally talk to local services.","Audit configured base URLs for hosts that resolve to private ranges (CI often differs from laptops)."],"tags":["network","ssrf","security","dns","egress"],"backgroundTag":"path-traversal-blocked","analyzedSha":"aac998c7ed7248ea853b720ceeb1fdbeb322fc5d","analyzedAt":"2026-09-16T06:35:29.214Z","contentChangedAt":"2026-09-16T06:35:29.214Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}