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
- Serve the JD from a public URL the tool is allowed to fetch.
- If the content is local, save it to a file and pass the file path instead of a URL.
- 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
- Never point the tool at localhost or .local hostnames — by design it will refuse.
- For local content, use the file/paste input path.
- Validate URLs against public DNS resolution before passing them.
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.
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- agentic-jobs: untrusted hostname
- eightfold: untrusted hostname
- landingjobs: untrusted hostname
- larajobs: untrusted hostname
- lever: untrusted hostname
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)