{"record":{"id":"78bfe81bde78a64f","repo":"santifer/career-ops","slug":"access-denied-egress-guard-blocked-private-target","errorCode":null,"errorMessage":"Access denied: Egress guard blocked private target IP ${ip}","messagePattern":"Access denied: Egress guard blocked private target IP (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"liveness-browser.mjs","lineNumber":236,"sourceCode":"}\n\n// Second layer of the egress guard: `rejectPrivateOrInvalid` only sees the\n// literal host, so a public hostname that *resolves* to private space still\n// gets through it. Resolve and re-check every address before the request is\n// allowed out. Exported so other Playwright callers (archive-posting.mjs) wire\n// up the same two-layer guard instead of growing a second implementation.\nexport async function validateUrlSecurity(urlString) {\n  const url = new URL(urlString.endsWith('.') ? urlString.slice(0, -1) : urlString);\n  const hostname = url.hostname;\n  const host = normalizeHost(hostname);\n  const addresses = await resolveDnsCached(host);\n  for (const ip of addresses) {\n    const norm = normalizeHost(ip);\n    const mapped = extractMappedIPv4(norm);\n    const candidates = mapped ? [norm, mapped] : [norm];\n    for (const candidate of candidates) {\n      if (PRIVATE_HOST_PATTERNS.some((pattern) => pattern.test(candidate))) {\n        throw new Error(`Access denied: Egress guard blocked private target IP ${ip}`);\n      }\n    }\n  }\n}\n\nexport async function checkUrlLiveness(page, url, { extraSettleMs = 0 } = {}) {\n  const guardError = rejectPrivateOrInvalid(url);\n  if (guardError) {\n    return { result: 'uncertain', code: guardError.code, reason: guardError.reason };\n  }\n  if (page) {\n    page._blockedByGuard = null;\n  }\n  if (page && typeof page.route === 'function' && !page._routeInterceptorRegistered) {\n    page._routeInterceptorRegistered = true;\n    await page.route('**/*', async (route) => {\n      const requestUrl = route.request().url();\n      const errGuard = rejectPrivateOrInvalid(requestUrl);","sourceCodeStart":218,"sourceCodeEnd":254,"githubUrl":"https://github.com/santifer/career-ops/blob/e7abd431fce9348a95261acac9e0c14779c35df8/liveness-browser.mjs#L218-L254","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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."],"exampleFix":"// before\nawait validateUrlSecurity('http://192.168.1.20/jobs/123');\n// after\nawait validateUrlSecurity('https://careers.example.com/jobs/123');","handlingStrategy":"validation","validationCode":"import { lookup } from 'node:dns/promises';\nconst PUBLIC_RE = /^(?!10\\.|127\\.|169\\.254\\.|172\\.(1[6-9]|2\\d|3[01])\\.|192\\.168\\.)/;\nfor (const ip of await lookup(host, { all: true })) {\n  if (!PUBLIC_RE.test(ip.address)) throw new Error(`${host} resolves to private IP ${ip.address}`);\n}","typeGuard":null,"tryCatchPattern":"try {\n  await checkUrlLiveness(page, url);\n} catch (err) {\n  if (err.message.includes('Egress guard blocked')) {\n    console.error(`${url} resolves to a private IP; verify it manually or use its public hostname.`);\n  } else throw err;\n}","preventionTips":["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."],"tags":["security","ssrf","dns-rebinding","network","liveness"],"backgroundTag":"permission-denied","analyzedSha":"e7abd431fce9348a95261acac9e0c14779c35df8","analyzedAt":"2026-09-16T06:35:29.214Z","contentChangedAt":"2026-09-16T06:35:29.214Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}