{"record":{"id":"81844cb0f7866812","repo":"santifer/career-ops","slug":"plugin-egress-to-hostname-is-blocked-private-l","errorCode":null,"errorMessage":"plugin egress to ${hostname} is blocked (private/loopback/metadata range)","messagePattern":"plugin egress to (.+?) is blocked \\(private/loopback/metadata range\\)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"plugins/_net.mjs","lineNumber":81,"sourceCode":"  if (a >= 224) return true;                      // multicast / reserved\n  return false;\n}\n\nconst LOOPBACK_HOSTS = new Set(['localhost', '127.0.0.1', '::1', '[::1]']);\n\n/**\n * Resolve a hostname and reject if ANY resolved address is blocked. Returns the\n * validated addresses. Throws on a blocked or unresolvable host.\n * @param {string} hostname\n * @param {{ allowsLocalhost?: boolean }} [opts]\n * @returns {Promise<string[]>}\n */\nexport async function resolveAndValidate(hostname, { allowsLocalhost = false } = {}) {\n  // An IP literal host: validate directly (no DNS).\n  if (isIP(hostname)) {\n    if (isBlockedIp(hostname)) {\n      if (allowsLocalhost && isLoopbackLiteral(hostname)) return [hostname];\n      throw new Error(`plugin egress to ${hostname} is blocked (private/loopback/metadata range)`);\n    }\n    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)) {","sourceCodeStart":63,"sourceCodeEnd":99,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/plugins/_net.mjs#L63-L99","documentation":"resolveAndValidate in the plugin egress guard resolves a hostname to IP addresses and blocks any address in private, loopback, link-local, or cloud-metadata ranges. For a hostname that is already an IP literal, it validates directly via isBlockedIp and throws this error when the IP falls in a blocked range. The guard exists to stop SSRF: plugins must not be able to reach internal services, localhost, or metadata endpoints (e.g. 169.254.169.254).","triggerScenarios":"Calling resolveAndValidate with (a) an IP-literal hostname in a private range (10.x, 192.168.x, 127.x, 169.254.x, etc.) without allowsLocalhost, or (b) such an IP with allowsLocalhost=true where the IP is private/metadata but not a loopback literal (the loopback exemption only covers 127.0.0.0/8-style literals).","commonSituations":"A plugin configured to call a local Ollama/LM Studio instance at 127.0.0.1 but the caller forgot to pass allowsLocalhost: true; a plugin pointed at an internal staging service on 10.x; a config using a metadata IP or a LAN address (192.168.1.x) as an API endpoint; typo'd base URL like http://0.0.0.0.","solutions":["If the target is a legitimate local AI provider (Ollama/LM Studio), pass { allowsLocalhost: true } and use a loopback literal (127.0.0.1) — private non-loopback IPs stay blocked.","Change the plugin's endpoint to a public hostname/IP; the block is intentional for anything private.","If the endpoint must be internal, move it behind an approved public gateway/proxy rather than weakening the guard.","Check the hostname value in the plugin config for typos (0.0.0.0, 169.254.x, LAN addresses)."],"exampleFix":"// before: localhost target blocked because allowsLocalhost not passed\nawait resolveAndValidate(\"127.0.0.1:11434\");\n\n// after: explicitly allow loopback for local providers\nawait resolveAndValidate(\"127.0.0.1\", { allowsLocalhost: true });","handlingStrategy":"validation","validationCode":"import { isIP } from 'net';\nconst BLOCKED_RE = /^(10\\.|127\\.|169\\.254\\.|192\\.168\\.|172\\.(1[6-9]|2\\d|3[01])\\.|0\\.0\\.0\\.0$)/;\nfunction isPublicEndpoint(hostname) {\n  const host = hostname.replace(/:\\d+$/, '');\n  if (isIP(host)) return !BLOCKED_RE.test(host);\n  return !['localhost', 'metadata.google.internal'].includes(host.toLowerCase());\n}\n// before calling: isPublicEndpoint(hostname) || explicitlyPassingAllowsLocalhost","typeGuard":"function isLoopbackLiteral(host) {\n  return isIP(host) !== 0 && /^(127\\.|::1$)/.test(host);\n}","tryCatchPattern":"try {\n  await resolveAndValidate(hostname, { allowsLocalhost });\n} catch (err) {\n  if (err.message.includes('is blocked (private/loopback/metadata range)')) {\n    console.error(`Endpoint ${hostname} is private/metadata — use a public endpoint or allowsLocalhost with 127.0.0.1`);\n  }\n  throw err;\n}","preventionTips":["Use public hostnames for plugin API endpoints; never point plugins at LAN or metadata IPs.","Pass allowsLocalhost: true only for legitimate local AI providers, with 127.0.0.1 as the host.","Validate configured URLs against blocked ranges at config-load time, not request time.","Treat this error as an SSRF signal — audit the plugin config that produced it."],"tags":["ssrf","security","network","plugin-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"}