santifer/career-ops · error · Error
Refusing private/loopback host: ${host}
Error message
Refusing private/loopback host: ${host} What it means
Thrown by assertSafeRemoteUrl() when a URL host matches a loopback, link-local, private, or cloud-metadata address: localhost, ::1, *.local, 127.0.0.0/8, 10.0.0.0/8, 192.168.0.0/16, 169.254.0.0/16, or 172.16.0.0/12. This is the SSRF guard that prevents the runner (often running in a cloud context) from being directed at internal/metadata services.
Source
Thrown at openrouter-runner.mjs:394
}
// ---------------------------------------------------------------------------
// Job page content fetcher (Playwright-first, plain fetch fallback)
// ---------------------------------------------------------------------------
// Reject unsafe fetch targets (SSRF defense-in-depth): http(s) only, never
// loopback / link-local / private / cloud-metadata hosts. URLs come from the
// user's own portals.yml / pipeline.md, but we still fail closed.
function assertSafeRemoteUrl(url) {
let u;
try { u = new URL(url); } catch { throw new Error(`Invalid URL: ${url}`); }
if (u.protocol !== 'https:' && u.protocol !== 'http:') {
throw new Error(`Refusing non-HTTP(S) URL: ${url}`);
}
const host = u.hostname.toLowerCase();
const blocked = host === 'localhost' || host === '::1' || host.endsWith('.local') ||
/^127\./.test(host) || /^10\./.test(host) || /^192\.168\./.test(host) ||
/^169\.254\./.test(host) || /^172\.(1[6-9]|2\d|3[01])\./.test(host);
if (blocked) throw new Error(`Refusing private/loopback host: ${host}`);
return u;
}
async function fetchJobPage(url) {
assertSafeRemoteUrl(url);
let chromium;
try {
({ chromium } = await import('playwright'));
} catch {
console.warn('[fetch] Playwright unavailable — falling back to plain fetch.');
}
if (chromium) {
let browser;
try {
browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30_000 });View on GitHub (pinned to 9b17a8ac97)
Solutions
- Point the entry at the public, externally-reachable URL for the same portal.
- If you must fetch an internal resource, do so outside career-ops (fetchJobPage deliberately cannot reach private hosts).
- For local testing, mock fetchJobPage or call parsePortals with rawOverride rather than hitting a loopback server.
- Verify no portals.yml company has an api/careers_url resolving to a private range.
Example fix
// before careers_url: "https://192.168.1.50/jobs" // after careers_url: "https://careers.company.com/jobs"
Defensive patterns
Strategy: validation
Validate before calling
const PRIVATE_HOST_RE = /^(localhost|::1)|\.local$|^(127\.|10\.|192\.168\.|169\.254\.|172\.(1[6-9]|2\d|3[01])\.)/i;
function isPublicHttpUrl(s) {
let u;
try { u = new URL(s); } catch { return false; }
if (u.protocol !== 'http:' && u.protocol !== 'https:') return false;
return !PRIVATE_HOST_RE.test(u.hostname);
} Type guard
/** True only for http(s) URLs whose literal hostname is public (not private/loopback). */
function isPublicHttpUrl(s) {
if (typeof s !== 'string') return false;
let u;
try { u = new URL(s); } catch { return false; }
if (u.protocol !== 'http:' && u.protocol !== 'https:') return false;
const h = u.hostname.toLowerCase();
return !(h === 'localhost' || h === '::1' || h.endsWith('.local') ||
/^127\./.test(h) || /^10\./.test(h) || /^192\.168\./.test(h) ||
/^169\.254\./.test(h) || /^172\.(1[6-9]|2\d|3[01])\./.test(h));
} Prevention
- Keep all portals.yml api/careers_url entries on public domains.
- Add a CI check that scans config for private/loopback hosts.
- Remember the guard is string-based — it will not catch a public hostname that DNS-resolves to a private IP.
When it happens
Trigger: A job URL in pipeline.md/portals.yml points at a private RFC1918 host; a staging/test portal is hosted on an internal domain like https://intranet.company.local; a host that DNS-resolves to a private IP (note: this guard is hostname-string based, not resolution based, so it only catches literal private hostnames).
Common situations: Local development with a portal mirror on http://localhost or 192.168.x.x; a .local mDNS hostname; a cloud-metadata-style endpoint accidentally copy-pasted; an intranet-only ATS that career-ops cannot reach from outside the corporate network anyway.
Related errors
- Refusing non-HTTP(S) URL: ${url}
- Access denied: Egress guard blocked private target IP ${ip}
- plugin egress to ${hostname} is blocked (private/loopback/me
- plugin egress: ${hostname} resolves to a blocked address (${
- DNS resolution returned no addresses for ${hostname}
AI-assisted analysis of santifer/career-ops@9b17a8ac97 (2026-08-13).
Data as JSON: /api/errors/8c2dbb3946db31ee.
Report an issue: GitHub.