{"record":{"id":"5181f29b3e573a7b","repo":"santifer/career-ops","slug":"access-denied-egress-guard-blocked-private-target-5181f2","errorCode":null,"errorMessage":"Access denied: Egress guard blocked private target IP ${ip}","messagePattern":"Access denied: Egress guard blocked private target IP (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"upskill.mjs","lineNumber":874,"sourceCode":"  const hostname = url.hostname;\n\n  if (hostname === 'localhost' || hostname.endsWith('.local')) {\n    throw new Error('Access denied: Localhost or internal domain target detected.');\n  }\n\n  let addresses;\n  if (dnsCache.has(hostname)) {\n    addresses = dnsCache.get(hostname);\n  } else {\n    addresses = await dns.resolve(hostname).catch(() => []);\n    const lookupRes = await dns.lookup(hostname).catch(() => null);\n    if (lookupRes) addresses.push(lookupRes.address);\n    dnsCache.set(hostname, addresses);\n  }\n\n  for (const ip of addresses) {\n    if (/^(127\\.|10\\.|192\\.168\\.|172\\.(1[6-9]|2[0-9]|3[0-1])\\.|169\\.254\\.)/.test(ip)) {\n      throw new Error(`Access denied: Egress guard blocked private target IP ${ip}`);\n    }\n    if (ip === '::1' || ip.startsWith('fe80:') || ip.startsWith('fc00:') || ip.startsWith('fd00:')) {\n      throw new Error(`Access denied: Egress guard blocked private target IPv6 ${ip}`);\n    }\n  }\n  return url.toString();\n}\n\n// --- CLI ---\n// Everything below runs ONLY when upskill.mjs is the process entry point.\n//\n// Without this guard the module tail was unconditional, so `import\n// { knownSkillsText } from './upskill.mjs'` re-parsed the IMPORTER's argv and ran\n// one of these branches. That made the pure helpers above un-unit-testable despite\n// their \"exported for unit testing\" docblocks — every assertion about them had to\n// live inside --self-test.\n//\n// Under tests/ it also broke the harness, because test-all.mjs imports discovered","sourceCodeStart":856,"sourceCodeEnd":892,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/upskill.mjs#L856-L892","documentation":"After resolving the hostname via DNS, validateUrlSecurity() checks every returned address against IPv4 private/reserved ranges (loopback 127/8, private 10/8 and 192.168/16, shared 172.16/12, link-local 169.254/16) and blocks the fetch. This prevents DNS-based SSRF where a public-looking hostname resolves to an internal IP. The message names the exact offending IP.","triggerScenarios":"A JD URL whose hostname's DNS A record resolves to any 127.x, 10.x, 192.168.x, 172.16–31.x, or 169.254.x address when upskill attempts the fetch.","commonSituations":"Corporate split-horizon DNS resolving an intranet host to a private IP; a DNS rebinding or misconfigured public DNS record pointing at a private address; testing with a tunnel bound to 127.0.0.1 behind a public hostname.","solutions":["Use a URL that resolves to a genuinely public IP (host the JD on a publicly reachable server).","If the content is internal, fetch it yourself and pass the saved file or pasted text instead of a URL.","If the DNS record is wrong (public domain → private IP), fix the DNS record and retry."],"exampleFix":"// before\nnode upskill.mjs --url https://intranet.example.com/jd  # resolves to 10.0.0.5\n// after\nnode upskill.mjs --file ./jd.txt","handlingStrategy":"validation","validationCode":"const { lookup } = await import('dns/promises');\nconst addrs = await lookup(new URL(url).hostname, { all: true });\nconst priv = /^(127\\.|10\\.|192\\.168\\.|172\\.(1[6-9]|2\\d|3[01])\\.|169\\.254\\.)/;\nif (addrs.some(a => priv.test(a.address))) console.error('hostname resolves to a private IPv4; use a public host or a file');","typeGuard":null,"tryCatchPattern":"try {\n  await upskillFromUrl(url);\n} catch (e) {\n  if (e.message.includes('blocked private target IP')) {\n    console.error('host resolves to a private IPv4; fetch the JD yourself and pass the file');\n  } else throw e;\n}","preventionTips":["Check DNS resolution (nslookup/dig) for the hostname before using it as a URL source.","Prefer file-based input for intranet or tunnel-hosted content.","Be alert to split-horizon DNS resolving public names to private IPs inside corporate networks."],"tags":["security","ssrf","dns","network"],"backgroundTag":"invalid-url","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"}