{"record":{"id":"83c6b17e47685658","repo":"santifer/career-ops","slug":"access-denied-egress-guard-blocked-private-target-83c6b1","errorCode":null,"errorMessage":"Access denied: Egress guard blocked private target IPv6 ${ip}","messagePattern":"Access denied: Egress guard blocked private target IPv6 (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"upskill.mjs","lineNumber":877,"sourceCode":"    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\n// suites IN-PROCESS and they therefore share its argv. Both branches were\n// reachable, and both were measured by pinning isMain to true:\n//   - ordinary argv → the aggregate branch walked the tracker and every linked","sourceCodeStart":859,"sourceCodeEnd":895,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/upskill.mjs#L859-L895","documentation":"The IPv6 counterpart of the egress guard: validateUrlSecurity() blocks hostnames resolving to ::1 (loopback), fe80::/10 (link-local), or fc00::/7 (unique-local) addresses. Same SSRF rationale as the IPv4 check — a fetch to a resolved IPv6 private address is refused before any request is made.","triggerScenarios":"A JD URL whose hostname's DNS AAAA record is ::1, fe80::/10, fc00::/7 (fc00::/fd00:: prefixes) — e.g. testing against an IPv6 loopback or a ULA-addressed internal host.","commonSituations":"Local dev servers advertising ::1 via /etc/hosts or mDNS; internal IPv6 networks using unique-local addresses; hosts with AAAA records pointing at private infrastructure while A records are public (the resolver returned the AAAA first).","solutions":["Point the URL at a host whose AAAA record is a public (globally routable) IPv6 address.","Prefer the file path / pasted-text input for internal or local content.","If DNS wrongly publishes a private AAAA record for a public domain, correct the DNS record."],"exampleFix":"// before\nnode upskill.mjs --url http://[fd00::10]:8080/jd\n// after\nnode upskill.mjs --file ./jd.md","handlingStrategy":"validation","validationCode":"const { lookup } = await import('dns/promises');\nconst addrs = await lookup(new URL(url).hostname, { all: true, family: 6 });\nconst bad = a => a === '::1' || a.startsWith('fe80:') || a.startsWith('fc00:') || a.startsWith('fd00:');\nif (addrs.some(a => bad(a.address))) console.error('hostname resolves to a private IPv6; 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 IPv6')) {\n    console.error('AAAA record is private (loopback/link-local/ULA); pass the JD as a file instead');\n  } else throw e;\n}","preventionTips":["Inspect AAAA records (dig AAAA host) before using IPv6-reachable hosts.","Remove stale /etc/hosts entries mapping hostnames to ::1 for hosts you intend to fetch.","Use the file input path for any content on ULA (fd00::/8) networks."],"tags":["security","ssrf","ipv6","dns"],"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"}