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
Thrown by upskill.mjs's SSRF egress guard (validateUrlSecurity) before any network access when the URL's hostname is exactly 'localhost' or ends with '.local'. The guard exists so a JD URL passed via --url-text (or as a bare URL argument) can never steer the tool at the local machine or an mDNS-style internal name. It is a deliberate fail-closed security block, not a malfunction.
Source
Thrown at upskill.mjs:797
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 60398d6549)
Solutions
- Use the public HTTPS URL of the job posting instead of a localhost/.local address
- If you control the test fixture, serve it from a hostname that is neither 'localhost' nor *.local and resolves publicly
- Pass the JD as saved text through the non-URL input paths of upskill instead of a URL
- Do not patch the guard out wholesale — it is SSRF protection; narrow any exception consciously
Example fix
# before node upskill.mjs --url-text http://localhost:3000/jd.txt # after node upskill.mjs --url-text https://boards.example.com/jobs/1234
Defensive patterns
Strategy: validation
Validate before calling
import { isEgressBlockedHostname } from './upskill.mjs'; // or inline:
function isBlockedHost(urlString) {
const clean = urlString.endsWith('.') ? urlString.slice(0, -1) : urlString;
const h = new URL(clean).hostname.toLowerCase();
return h === 'localhost' || h.endsWith('.local');
}
if (isBlockedHost(inputUrl)) throw new Error(`refusing internal target: ${inputUrl}`); Type guard
function isPublicHttpUrl(urlString) {
try {
const u = new URL(urlString.endsWith('.') ? urlString.slice(0, -1) : urlString);
return /^https?:$/.test(u.protocol) && u.hostname !== 'localhost' && !u.hostname.endsWith('.local');
} catch {
return false;
}
} Try / catch
try {
await validateUrlSecurity(url);
} catch (err) {
if (String(err.message).startsWith('Access denied: Localhost')) {
// policy block: surface to caller, do not retry
throw new Error(`internal target refused: ${url}`);
}
throw err;
} Prevention
- Never pass localhost/.local or intranet URLs to upskill's URL mode
- Keep a whitelist of public ATS hosts and validate input against it before invoking
- For local fixtures, prefer file-based input over an HTTP URL
When it happens
Trigger: Running `node upskill.mjs --url-text http://localhost:3000/jd` or `node upskill.mjs https://intranet.local/posting`. The check fires on the URL() hostname after stripping one trailing dot, so 'localhost.', 'http://localhost:8080/x', and '*.local' names all trigger it. It also fires per-request inside the Playwright route handler for every subresource and redirect hop.
Common situations: Pointing the tool at a local dev server or LAN staging box while testing; corporate intranet hostnames under .local; pasting an internal ATS sandbox link instead of the public posting URL; a JD page whose redirects or assets reference internal .local hosts.
Understand the failure class
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- refusing to archive restricted destination: ${preGuard.reaso
- Invalid or blocked URL: ${rejected.reason}
- 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}
AI-assisted analysis of santifer/career-ops@60398d6549 (2026-08-20).
Data as JSON: /api/errors/4ad400b40b242a2f.
Report an issue: GitHub.