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

  1. Use the URL's public hostname (its public DNS name) rather than an internal IP or localhost.
  2. If the posting is only reachable on an internal network, verify it manually in a browser — the guard intentionally cannot reach it.
  3. Check DNS: `dig +short host` and confirm the answer is a public IP; fix the record or use the correct public endpoint.
  4. 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

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.

Related errors


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)