santifer/career-ops · error

Access denied: Egress guard blocked private target IP

Error message

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

What it means

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.

Solutions

  1. Use a URL that resolves to a genuinely public IP (host the JD on a publicly reachable server).
  2. If the content is internal, fetch it yourself and pass the saved file or pasted text instead of a URL.
  3. If the DNS record is wrong (public domain → private IP), fix the DNS record and retry.

Example fix

// before
node upskill.mjs --url https://intranet.example.com/jd  # resolves to 10.0.0.5
// after
node upskill.mjs --file ./jd.txt
Defensive patterns

Strategy: validation

Validate before calling

const { lookup } = await import('dns/promises');
const addrs = await lookup(new URL(url).hostname, { all: true });
const priv = /^(127\.|10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.|169\.254\.)/;
if (addrs.some(a => priv.test(a.address))) console.error('hostname resolves to a private IPv4; use a public host or a file');

Try / catch

try {
  await upskillFromUrl(url);
} catch (e) {
  if (e.message.includes('blocked private target IP')) {
    console.error('host resolves to a private IPv4; fetch the JD yourself and pass the file');
  } else throw e;
}

Prevention

When it happens

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

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

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/5181f29b3e573a7b. Report an issue: GitHub.

Appendix: source

Thrown at upskill.mjs:874

  const hostname = url.hostname;

  if (hostname === 'localhost' || hostname.endsWith('.local')) {
    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

View on GitHub (pinned to aac998c7ed)