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

  1. Verify what the blocked hostname resolves to: `dig +short <host>` — if private, the page asset genuinely is internal and its loss is usually cosmetic
  2. Fix local DNS (disconnect VPN / remove hosts-file overrides) if the name should be public, then re-run archive-posting
  3. 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

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


AI-assisted analysis of santifer/career-ops@60398d6549 (2026-08-20). Data as JSON: /api/errors/831e78bf354b7bb9. Report an issue: GitHub.