santifer/career-ops · error · Error
Access denied: Egress guard blocked private target IP
Error message
Access denied: Egress guard blocked private target IP ${ip} What it means
validateUrlSecurity() in the liveness browser checker resolves the target URL's host to IP addresses and rejects any that match private/internal ranges (loopback, RFC1918, link-local, mapped IPv4, etc.). This DNS-rebinding/SSRF egress guard throws when the hostname resolves to a private IP, refusing to navigate.
Solutions
- Use the URL's public hostname (its public DNS name) rather than an internal IP or localhost.
- If the posting is only reachable on an internal network, verify it manually in a browser — the guard intentionally cannot reach it.
- Check DNS: `dig +short host` and confirm the answer is a public IP; fix the record or use the correct public endpoint.
- Do not bypass the guard — it exists to block SSRF/DNS-rebinding against private infrastructure.
Example fix
// before
await validateUrlSecurity('http://192.168.1.20/jobs/123');
// after
await validateUrlSecurity('https://careers.example.com/jobs/123'); Defensive patterns
Strategy: validation
Validate before calling
import { lookup } from 'node:dns/promises';
const PUBLIC_RE = /^(?!10\.|127\.|169\.254\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.)/;
for (const ip of await lookup(host, { all: true })) {
if (!PUBLIC_RE.test(ip.address)) throw new Error(`${host} resolves to private IP ${ip.address}`);
} Try / catch
try {
await checkUrlLiveness(page, url);
} catch (err) {
if (err.message.includes('Egress guard blocked')) {
console.error(`${url} resolves to a private IP; verify it manually or use its public hostname.`);
} else throw err;
} Prevention
- Only pass public, DNS-resolvable https:// URLs to the liveness checker.
- Resolve the host yourself first and reject private ranges before calling.
- Remember VPN/staging hosts on private networks are intentionally unreachable.
When it happens
Trigger: Calling checkUrlLiveness()/validateUrlSecurity() with a URL whose host resolves to a private address: http://localhost, http://127.0.0.1, http://10.x.x.x, http://192.168.x.x, an internal hostname, or a public DNS name that resolves to a private IP (DNS rebinding or split-horizon DNS).
Common situations: Testing liveness against a staging URL only reachable on the VPN; using an internal careers portal behind corporate DNS; a misconfigured DNS record pointing a public domain at a private IP; localhost testing.
Understand the failure class
Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- Access denied: Egress guard blocked private target IP
- Blocked request to restricted destination
- plugin egress: resolves to a blocked address ( ) — possible…
- plugin egress to is blocked (private/loopback/metadata…
- Refusing private/loopback host
AI-assisted analysis of santifer/career-ops@e7abd431fc (2026-09-16).
Data as JSON: /api/errors/78bfe81bde78a64f.
Report an issue: GitHub.
Appendix: source
Thrown at liveness-browser.mjs:236
}
// Second layer of the egress guard: `rejectPrivateOrInvalid` only sees the
// literal host, so a public hostname that *resolves* to private space still
// gets through it. Resolve and re-check every address before the request is
// allowed out. Exported so other Playwright callers (archive-posting.mjs) wire
// up the same two-layer guard instead of growing a second implementation.
export async function validateUrlSecurity(urlString) {
const url = new URL(urlString.endsWith('.') ? urlString.slice(0, -1) : urlString);
const hostname = url.hostname;
const host = normalizeHost(hostname);
const addresses = await resolveDnsCached(host);
for (const ip of addresses) {
const norm = normalizeHost(ip);
const mapped = extractMappedIPv4(norm);
const candidates = mapped ? [norm, mapped] : [norm];
for (const candidate of candidates) {
if (PRIVATE_HOST_PATTERNS.some((pattern) => pattern.test(candidate))) {
throw new Error(`Access denied: Egress guard blocked private target IP ${ip}`);
}
}
}
}
export async function checkUrlLiveness(page, url, { extraSettleMs = 0 } = {}) {
const guardError = rejectPrivateOrInvalid(url);
if (guardError) {
return { result: 'uncertain', code: guardError.code, reason: guardError.reason };
}
if (page) {
page._blockedByGuard = null;
}
if (page && typeof page.route === 'function' && !page._routeInterceptorRegistered) {
page._routeInterceptorRegistered = true;
await page.route('**/*', async (route) => {
const requestUrl = route.request().url();
const errGuard = rejectPrivateOrInvalid(requestUrl);View on GitHub (pinned to e7abd431fc)