{"record":{"id":"95dc9e618c1db51a","repo":"santifer/career-ops","slug":"blocked-request-to-restricted-destination-dns-95dc9e","errorCode":null,"errorMessage":"Blocked request to restricted destination (DNS): ${requestUrl} - ${err.message}","messagePattern":"Blocked request to restricted destination \\(DNS\\): (.+?) - (.+?)","errorType":"console","errorClass":null,"httpStatus":null,"severity":"info","filePath":"liveness-browser.mjs","lineNumber":264,"sourceCode":"  }\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);\n      if (errGuard) {\n        console.warn(`Blocked request to restricted destination: ${requestUrl}`);\n        page._blockedByGuard = errGuard;\n        return route.abort('blockedbyclient');\n      }\n      try {\n        await validateUrlSecurity(requestUrl);\n        return route.continue();\n      } catch (err) {\n        console.warn(`Blocked request to restricted destination (DNS): ${requestUrl} - ${err.message}`);\n        // A host that resolves to nothing is a DEAD THIRD-PARTY SCRIPT, not a\n        // statement about the posting. Measured 2026-08-14 over a 217-URL\n        // recheck: 78 live postings were returned as `uncertain` because an\n        // analytics or ad host on the page no longer exists — 53 on\n        // personalisation.visitorqueue.com, 17 on s7.addthis.com (AddThis was\n        // shut down in 2023), the rest on fluidads and cloudfront. One was\n        // opened by hand to confirm: 11,178 characters of live posting and a\n        // working apply control, called uncertain because of a dead tracker.\n        //\n        // The request is still aborted either way, so the egress guard loses\n        // nothing. Only the VERDICT stops being poisoned, and only for a\n        // subresource: if the main document itself cannot resolve, that is a\n        // real finding about the posting and still counts.\n        // Whether this was the main document or a subresource can only be asked\n        // of a real Playwright request. Callers may pass a lighter route double\n        // (the test suite does, with request() returning just a url()), and for\n        // those the answer is unknowable — so default to TRUE, which keeps the\n        // pre-existing behaviour of poisoning the verdict. The relaxation only","sourceCodeStart":246,"sourceCodeEnd":282,"githubUrl":"https://github.com/santifer/career-ops/blob/60398d6549a46f5266929538af21cfab94badc75/liveness-browser.mjs#L246-L282","documentation":"The guard's second layer — validateUrlSecurity — resolved the request's hostname with real DNS (resolve4 + resolve6 + lookup) and either got no addresses at all (dead host, tagged livenessCode 'dns_no_addresses') or got addresses inside private/loopback space ('Access denied: Egress guard blocked private target IP', a DNS-rebinding-shaped hit). The request is aborted either way. Per the 2026-08-14 measurement note in the source, a dead third-party host (shut-down trackers like addthis) must NOT poison the verdict — only the main document failing to resolve says the posting itself is dead.","triggerScenarios":"The page references analytics hosts that no longer exist (ENOTFOUND → dns_no_addresses); a public hostname resolves into 10.x/127.0.0.1/169.254.x (rebinding — hard block); corporate DNS, VPN split-DNS, or a captive portal resolves every name to an internal IP, so every subresource trips the guard.","commonSituations":"Old job postings with dead trackers; VPN split-DNS wildcards; captive portals resolving all hosts to a login IP; restrictive corporate resolvers.","solutions":["Treat as noise if the verdict is still live/dead based on the main document — dead subresource hosts are expected on old pages.","If MANY hosts trip it or everything returns uncertain, check your own DNS: dig the blocked hostname and look for wildcard or internal answers (VPN, captive portal).","Confirm the posting's own hostname resolves publicly before re-running the check."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"import dns from 'node:dns/promises';\nconst addrs = await dns.lookup(host, { all: true }).catch(() => []);\nconst isPrivate = (a) => /^(10\\.|127\\.|169\\.254\\.|192\\.168\\.|172\\.(1[6-9]|2\\d|3[01])\\.|::1|f[cd])/i.test(a);\nif (addrs.length === 0 || addrs.some((x) => isPrivate(x.address))) {\n  console.warn(`host ${host} is dead or resolves privately — expect guard blocks`);\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Run liveness checks from a network with clean public DNS — no captive portals or VPN split-DNS wildcards","Remember DNS answers are memoized per process (dnsCache); a stale block persists until restart","Distinguish dns_no_addresses (dead tracker, ignorable) from 'blocked private target IP' (real rebinding signal)"],"tags":["dns","ssrf","egress-guard","dead-tracker","captive-portal"],"backgroundTag":"dns-resolution-failed","analyzedSha":"60398d6549a46f5266929538af21cfab94badc75","analyzedAt":"2026-08-20T23:00:06.764Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}