santifer/career-ops · error

Access denied: Egress guard blocked private target IPv6

Error message

Access denied: Egress guard blocked private target IPv6 ${ip}

What it means

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.

Solutions

  1. Point the URL at a host whose AAAA record is a public (globally routable) IPv6 address.
  2. Prefer the file path / pasted-text input for internal or local content.
  3. If DNS wrongly publishes a private AAAA record for a public domain, correct the DNS record.

Example fix

// before
node upskill.mjs --url http://[fd00::10]:8080/jd
// after
node upskill.mjs --file ./jd.md
Defensive patterns

Strategy: validation

Validate before calling

const { lookup } = await import('dns/promises');
const addrs = await lookup(new URL(url).hostname, { all: true, family: 6 });
const bad = a => a === '::1' || a.startsWith('fe80:') || a.startsWith('fc00:') || a.startsWith('fd00:');
if (addrs.some(a => bad(a.address))) console.error('hostname resolves to a private IPv6; use a public host or a file');

Try / catch

try {
  await upskillFromUrl(url);
} catch (e) {
  if (e.message.includes('blocked private target IPv6')) {
    console.error('AAAA record is private (loopback/link-local/ULA); pass the JD as a file instead');
  } else throw e;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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).

Understand the failure class

Background: "Invalid URL" / "URL cannot be empty": fix the malformed or missing URL behind request-construction failures — this error's family across 50 libraries.

Related errors


AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16). Data as JSON: /api/errors/83c6b17e47685658. Report an issue: GitHub.

Appendix: source

Thrown at upskill.mjs:877

    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 linked

View on GitHub (pinned to aac998c7ed)