santifer/career-ops · error · Error

plugin egress: resolves to a blocked address ( ) — possible…

Error message

plugin egress: ${hostname} resolves to a blocked address (${address}) — possible SSRF/rebinding

What it means

After resolving the hostname, the egress guard checks every returned address against an SSRF blocklist (private/link-local/loopback ranges). If any resolved IP is blocked — and the loopback exemption (allowsLocalhost) does not apply — the request is aborted with this error rather than being sent to a protected address. This defends against SSRF and DNS-rebinding attacks where a public-looking hostname resolves to an internal IP.

Solutions

  1. Determine the resolved IP (`dig <host> +short`) and confirm whether it should be reachable; if it is a private/internal address by design, run with the localhost/internal allowance enabled (the allowsLocalhost path exempts loopback literals).
  2. If the hostname is wrong, fix the config to use the public endpoint (e.g. the real external API host instead of an internal one).
  3. Expose the internal service through an approved public gateway/proxy instead of addressing it by internal DNS.
  4. If you are being hit by rebinding-style records (mixed public/private answers), pin the host to a vetted static address in /etc/hosts under your control.

Example fix

// before (resolves to 10.0.0.4 → blocked)
await pluginFetch('https://internal.corp/api');
// after: enable the local/internal allowance or use the public endpoint
await pluginFetch('https://api.public.example.com/api');
Defensive patterns

Strategy: validation

Validate before calling

const dns = require('dns').promises;
const net = require('net');
function isPrivate(ip) {
  if (net.isIP(ip) === 0) return true;
  if (ip.startsWith('127.') || ip.startsWith('10.') || ip.startsWith('192.168.') ||
      ip.startsWith('169.254.')) return true;
  const m = ip.match(/^172\.(1[6-9]|2\d|3[01])\./);
  return !!m;
}
async function hostIsPublic(host) {
  const addrs = await dns.lookup(host, { all: true });
  return addrs.length > 0 && addrs.every(a => !isPrivate(a.address));
}
// call hostIsPublic(host) before the request

Type guard

function isLoopbackLiteralStr(ip) {
  return typeof ip === 'string' && /^127\./.test(ip);
}

Try / catch

try {
  return await pluginFetch(url);
} catch (err) {
  if (String(err.message).includes('blocked address')) {
    throw new Error(`EGRESS_BLOCKED: ${url} targets a private/metadata IP; use the public endpoint or enable the localhost allowance`);
  }
  throw err;
}

Prevention

When it happens

Trigger: Any plugin network call whose hostname resolves to a blocked IP: hostnames pointing at 127.x, 10.x, 172.16-31.x, 192.168.x, link-local 169.254.x, or metadata endpoints like 169.254.169.254; also DNS-rebinding names that alternate between public and private answers.

Common situations: Pointing a plugin at an internal service by DNS name while local network access is disallowed; testing against 'localhost' or 'host.docker.internal' without the localhost allowance; a misconfigured internal DNS zone; using a hostname that rebinding-rotates to 127.0.0.1.

Understand the failure class

Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.

Related errors


AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16). Data as JSON: /api/errors/0983afde2918d309. Report an issue: GitHub.

Appendix: source

Thrown at plugins/_net.mjs:101

    return [hostname];
  }

  if (allowsLocalhost && LOOPBACK_HOSTS.has(hostname.toLowerCase())) {
    // Local-AI providers (Ollama/LM Studio). Resolve but allow loopback through.
    return ['127.0.0.1'];
  }

  let addrs;
  try {
    addrs = await dnsLookup(hostname, { all: true });
  } catch (err) {
    throw new Error(`plugin egress: cannot resolve ${hostname} — ${err.message}`);
  }
  if (!addrs.length) throw new Error(`plugin egress: ${hostname} resolved to no addresses`);
  for (const { address } of addrs) {
    if (isBlockedIp(address)) {
      if (allowsLocalhost && isLoopbackLiteral(address)) continue;
      throw new Error(`plugin egress: ${hostname} resolves to a blocked address (${address}) — possible SSRF/rebinding`);
    }
  }
  return addrs.map(a => a.address);
}

function isLoopbackLiteral(ip) {
  if (ip === '::1') return true;
  if (isIP(ip) === 4) return ip.split('.')[0] === '127';
  return false;
}

View on GitHub (pinned to aac998c7ed)