santifer/career-ops · error
Access denied: Egress guard blocked private target IPv6 ${ip
Error message
Access denied: Egress guard blocked private target IPv6 ${ip} What it means
The IPv6 arm of upskill.mjs's SSRF egress guard: thrown when the target hostname resolves to ::1 (IPv6 loopback), a link-local fe80: address, or unique-local fc00:/fd00: addresses. It runs in the same address loop right after the IPv4 regex check and exists because IPv6 private ranges are missed by the IPv4-only regex. Mixed output from dns.resolve/dns.lookup means one AAAA record is enough to trip it.
Source
Thrown at upskill.mjs:815
throw new Error('Access denied: Localhost or internal domain target detected.');
}
let addresses;
if (dnsCache.has(hostname)) {
addresses = dnsCache.get(hostname);
} else {
addresses = await dns.resolve(hostname).catch(() => []);
const lookupRes = await dns.lookup(hostname).catch(() => null);
if (lookupRes) addresses.push(lookupRes.address);
dnsCache.set(hostname, addresses);
}
for (const ip of addresses) {
if (/^(127\.|10\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[0-1])\.|169\.254\.)/.test(ip)) {
throw new Error(`Access denied: Egress guard blocked private target IP ${ip}`);
}
if (ip === '::1' || ip.startsWith('fe80:') || ip.startsWith('fc00:') || ip.startsWith('fd00:')) {
throw new Error(`Access denied: Egress guard blocked private target IPv6 ${ip}`);
}
}
return url.toString();
}
// --- CLI ---
// Everything below runs ONLY when upskill.mjs is the process entry point.
//
// Without this guard the module tail was unconditional, so `import
// { knownSkillsText } from './upskill.mjs'` re-parsed the IMPORTER's argv and ran
// one of these branches. That made the pure helpers above un-unit-testable despite
// their "exported for unit testing" docblocks — every assertion about them had to
// live inside --self-test.
//
// Under tests/ it also broke the harness, because test-all.mjs imports discovered
// suites IN-PROCESS and they therefore share its argv. Both branches were
// reachable, and both were measured by pinning isMain to true:
// - ordinary argv → the aggregate branch walked the tracker and every linkedView on GitHub (pinned to 60398d6549)
Solutions
- Check AAAA records explicitly: `dig AAAA <hostname>` — confirm whether the name maps into ::1/fe80::/fc00::/fd00::
- Use the public posting URL whose AAAA record is a global unicast address, or force the IPv4 public name
- If DNS64/ULA is rewriting resolution, fix resolver configuration or query external resolvers, then re-run (dnsCache is per-process)
- Note the guard intentionally does not cover every IPv6 private form (e.g. IPv4-mapped ::ffff:10.0.0.1) — still never feed it internal targets on purpose
Example fix
# before node upskill.mjs --url-text https://mirror.internal.example/jd # AAAA fd00::5 -> blocked # after node upskill.mjs --url-text https://example.com/careers/123
Defensive patterns
Strategy: validation
Validate before calling
function isPrivateIPv6(ip) {
return ip === '::1' || ip.startsWith('fe80:') || ip.startsWith('fc00:') || ip.startsWith('fd00:');
}
import { lookup } from 'dns/promises';
const { address: first } = await lookup(hostname, { all: true }).then((r) => ({ address: r[0]?.address })) .catch(() => ({}));
if (first && isPrivateIPv6(first)) throw new Error(`private IPv6 target: ${first}`); Type guard
function isPrivateIPv6(ip) {
const lb = ip.toLowerCase();
return lb === '::1' || lb.startsWith('fe80:') || lb.startsWith('fc00:') || lb.startsWith('fd00:');
} Try / catch
try {
await validateUrlSecurity(url);
} catch (err) {
if (String(err.message).includes('blocked private target IPv6')) {
throw new Error(`IPv6-private resolution refused: ${url}`); // policy: never retry internal targets
}
throw err;
} Prevention
- Check AAAA records (dig AAAA) for dual-stack hostnames in ULA-heavy networks
- Beware DNS64 environments mapping names into fd00::/8
- Per-process DNS cache: restart the process after changing DNS entries
When it happens
Trigger: A --url-text/bare-URL hostname with an AAAA record for ::1, fe80::, fc00::, or fd00:: — e.g. a dual-stack intranet name, a DNS64/NAT64 environment mapping names into fd00::/8 ULA space, or a hosts entry with an IPv6 literal. Also fires per-request inside the Playwright route handler for subresources/redirects resolving to IPv6 private space.
Common situations: IPv6-enabled corporate networks using ULA (fd00::/8) addressing; DNS64 setups; local hosts files with ::1 entries for named vhosts; CI runners with IPv6-preferencing resolvers.
Understand the failure class
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- Access denied: Egress guard blocked private target IP ${ip}
- plugin egress: ${hostname} resolves to a blocked address (${
- Access denied: Egress guard blocked private target IP ${ip}
- Access denied: Localhost or internal domain target detected.
- Blocked request to restricted destination (DNS): ${reques
AI-assisted analysis of santifer/career-ops@60398d6549 (2026-08-20).
Data as JSON: /api/errors/83c6b17e47685658.
Report an issue: GitHub.