{"record":{"id":"48709bbe0da5b1e1","repo":"santifer/career-ops","slug":"blocked-request-to-restricted-destination-re","errorCode":null,"errorMessage":"   Blocked request to restricted destination: ${requestUrl} (${verdict.reason})","messagePattern":"   Blocked request to restricted destination: (.+?) \\((.+?)\\)","errorType":"console","errorClass":null,"httpStatus":null,"severity":"info","filePath":"archive-posting.mjs","lineNumber":289,"sourceCode":"\n/**\n * Register the egress guard on a Playwright context.\n *\n * Registered on the *context* rather than the page: a route bound to a single\n * page doesn't cover requests the flow makes outside it, and the context is\n * what owns the whole navigation. Both layers of the shared guard run here —\n * the literal-host check first (cheap, no network), then the DNS re-check that\n * catches a public hostname resolving into private space.\n *\n * @param {import('playwright').BrowserContext} context - Context to guard.\n */\nexport async function installEgressGuard(context) {\n  await context.route('**/*', async (route) => {\n    const requestUrl = route.request().url();\n\n    const verdict = rejectPrivateOrInvalid(requestUrl);\n    if (verdict) {\n      console.warn(`   Blocked request to restricted destination: ${requestUrl} (${verdict.reason})`);\n      return route.abort('blockedbyclient');\n    }\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      return route.abort('blockedbyclient');\n    }\n  });\n}\n\nexport async function archiveUrl(browser, url, { company: companyHint, role: roleHint } = {}) {\n  console.log(`\\n🔗  ${url}`);\n\n  // Refuse before launching any navigation, so an obviously-internal target\n  // never reaches Playwright at all.","sourceCodeStart":271,"sourceCodeEnd":307,"githubUrl":"https://github.com/santifer/career-ops/blob/60398d6549a46f5266929538af21cfab94badc75/archive-posting.mjs#L271-L307","documentation":"installEgressGuard in archive-posting.mjs — the shared two-layer guard re-exported from liveness-browser.mjs — aborted a request made while archiving a JD. Layer one (rejectPrivateOrInvalid) matched a literal private or invalid host, or layer two (validateUrlSecurity) resolved the hostname and found it dead or resolving into private space. This is the archiver's SSRF protection operating as designed; the navigation continues without the blocked subresource.","triggerScenarios":"The archived posting page requests trackers or scripts on localhost, RFC1918/link-local IPs, .local hosts, or non-http(s) schemes; a public hostname resolves to a private IP (DNS rebinding); a dead analytics host fails DNS resolution.","commonSituations":"Archiving older postings whose trackers shut down long ago; pages referencing internal-only analytics collectors; corporate DNS wildcards resolving external names to internal IPs.","solutions":["No action — subresource blocks do not affect the archived capture.","If the archive fails outright, check whether the posting URL itself (the main frame) was blocked — that URL must be public.","Keep the guard; these lines are audit telemetry of exactly what was refused."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"import { rejectPrivateOrInvalid, validateUrlSecurity } from './liveness-browser.mjs';\nconst preflight =\n  rejectPrivateOrInvalid(url) ??\n  (await validateUrlSecurity(url).then(() => null, (e) => e));\nif (preflight) throw new Error(`Refusing non-public archive URL up front: ${url}`);","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Only archive public http(s) posting URLs","Expect these lines on older postings with dead trackers — they are guard telemetry, not capture failures","Reuse installEgressGuard rather than writing a second guard, so blocks stay consistent across tools"],"tags":["ssrf","egress-guard","archive-posting","playwright"],"backgroundTag":"ssrf-private-address-blocked","analyzedSha":"60398d6549a46f5266929538af21cfab94badc75","analyzedAt":"2026-08-20T23:00:06.764Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}