santifer/career-ops · error

Access denied: Localhost or internal domain target detected.

Error message

Access denied: Localhost or internal domain target detected.

What it means

upskill.mjs's validateUrlSecurity() is an SSRF egress guard that vetoes URLs aimed at localhost or internal .local domains before any fetch occurs. It runs DNS resolution afterward and blocks private IPs, but this specific check is the hostname-based first line. It exists so untrusted JD URLs can never be used to probe internal services.

Solutions

  1. Serve the JD from a public URL the tool is allowed to fetch.
  2. If the content is local, save it to a file and pass the file path instead of a URL.
  3. If the host is a real public host but named under .local (misconfigured DNS), use its public FQDN or IP-reachable domain instead.

Example fix

// before
node upskill.mjs --url http://localhost:3000/job/123
// after
node upskill.mjs --file ./job-123.html
Defensive patterns

Strategy: validation

Validate before calling

function isBlockedHost(u) {
  const h = new URL(u).hostname;
  return h === 'localhost' || h.endsWith('.local');
}
if (isBlockedHost(url)) console.error('use a public URL or a local file path instead');

Try / catch

try {
  await upskillFromUrl(url);
} catch (e) {
  if (e.message.includes('Localhost or internal domain')) {
    console.error('save the JD locally and use --file instead');
  } else throw e;
}

Prevention

When it happens

Trigger: Passing a URL whose hostname is `localhost`, `*.localhost`, or ends in `.local` (e.g. http://localhost:8080/jd, http://myserver.local/x) as the --url/input source to upskill.

Common situations: Testing the tool against a locally running JD server; an internal corporate portal using a .local mDNS hostname; a typo'd or template URL left as localhost in a script.

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/4ad400b40b242a2f. Report an issue: GitHub.

Appendix: source

Thrown at upskill.mjs:859

  if (failures.length > 0) {
    console.error(`upskill self-test failed: ${failures.join('; ')}`);
    process.exit(1);
  }
  console.log('upskill self-test OK (extraction, suppression guards, weighting, tiering, report parsing, known-skills comment handling)');
  process.exit(0);
}

// Helper function to enforce egress guard against SSRF (Private/Loopback IPs)
const dnsCache = new Map();

async function validateUrlSecurity(urlString) {
  const dns = await import('dns/promises');
  const url = new URL(urlString.endsWith('.') ? urlString.slice(0, -1) : urlString);
  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}`);

View on GitHub (pinned to aac998c7ed)