santifer/career-ops · warning
Blocked request to restricted destination (DNS): ${reques
Error message
Blocked request to restricted destination (DNS): ${requestUrl} - ${err.message} What it means
Not a thrown exception but a warn+abort in archive-posting.mjs's Playwright route handler: after the syntactic rejectPrivateOrInvalid() check passes, validateUrlSecurity(requestUrl) DNS-resolves every request URL; if it resolves to a private/loopback IP the request is aborted ('blockedbyclient') and this warning is printed. It is the per-request, per-hop continuation of the SSRF egress guard during page.archive navigation.
Source
Thrown at archive-posting.mjs:297
* catches a public hostname resolving into private space.
*
* @param {import('playwright').BrowserContext} context - Context to guard.
*/
export async function installEgressGuard(context) {
await context.route('**/*', async (route) => {
const requestUrl = route.request().url();
const verdict = rejectPrivateOrInvalid(requestUrl);
if (verdict) {
console.warn(` Blocked request to restricted destination: ${requestUrl} (${verdict.reason})`);
return route.abort('blockedbyclient');
}
try {
await validateUrlSecurity(requestUrl);
return route.continue();
} catch (err) {
console.warn(` Blocked request to restricted destination (DNS): ${requestUrl} - ${err.message}`);
return route.abort('blockedbyclient');
}
});
}
export async function archiveUrl(browser, url, { company: companyHint, role: roleHint } = {}) {
console.log(`\n🔗 ${url}`);
// Refuse before launching any navigation, so an obviously-internal target
// never reaches Playwright at all.
const preGuard = rejectPrivateOrInvalid(url);
if (preGuard) {
throw new Error(`refusing to archive restricted destination: ${preGuard.reason}`);
}
const context = await browser.newContext();
await installEgressGuard(context);
const page = await context.newPage();View on GitHub (pinned to 60398d6549)
Solutions
- Verify what the blocked hostname resolves to: `dig +short <host>` — if private, the page asset genuinely is internal and its loss is usually cosmetic
- Fix local DNS (disconnect VPN / remove hosts-file overrides) if the name should be public, then re-run archive-posting
- Inspect the archived PDF: if a blocked subresource broke rendering, archive from the direct ATS job URL instead of the marketing careers page
Example fix
# before node archive-posting.mjs https://careers.example.com/jobs/123 # asset host resolves to 10.0.0.9 -> blocked per-request # after dig +short assets.example.com # confirm split-horizon DNS, fix resolver, then re-run: node archive-posting.mjs https://boards.example.com/companies/example/jobs/123
Defensive patterns
Strategy: validation
Validate before calling
import { lookup } from 'dns/promises';
async function resolvesPrivately(hostname) {
const addrs = await lookup(hostname, { all: true }).catch(() => []);
const priv = (a) => /^(127\.|10\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[0-1])\.|169\.254\.)/.test(a) || a === '::1' || /^(fe80:|fc00:|fd00:)/.test(a);
return addrs.some((x) => priv(x.address));
}
if (await resolvesPrivately(new URL(pageUrl).hostname)) throw new Error('target resolves privately'); Try / catch
// route handler pattern already used by archive-posting.mjs
await route.request().url();
try {
await validateUrlSecurity(requestUrl);
await route.continue();
} catch (err) {
console.warn(`blocked: ${requestUrl} (${err.message})`);
await route.abort('blockedbyclient'); // never follow private targets
} Prevention
- Feed archive-posting only public ATS URLs, never intranet mirrors
- Suspect split-horizon/wildcard DNS when public-looking names get blocked — verify with dig +short
- Archive from the direct job-posting URL so blocked internal subresources cannot break the capture
When it happens
Trigger: Calling archiveUrl(browser, url) where the page (or any subresource/redirect) references a hostname resolving to 127/8, 10/8, 172.16/12, 192.168/16, 169.254/16, ::1, fe80::/10, or fc00::/7 — e.g. split-horizon DNS, wildcard DNS to a router IP, or an internal CDN asset on the posting page. The main navigation still proceeds; individual requests get dropped.
Common situations: Archiving a posting whose careers page pulls telemetry or assets from an internal-only host; VPN split-horizon DNS making public-looking names private; DNS hijacking ISPs; localhost-linked fonts/scripts in self-hosted ATS instances.
Related errors
- refusing to archive restricted destination: ${preGuard.reaso
- refusing to archive restricted destination after redirect: $
- Access denied: Egress guard blocked private target IP ${ip}
- plugin egress: ${hostname} resolves to a blocked address (${
- Access denied: Egress guard blocked private target IP ${ip}
AI-assisted analysis of santifer/career-ops@60398d6549 (2026-08-20).
Data as JSON: /api/errors/831e78bf354b7bb9.
Report an issue: GitHub.