santifer/career-ops · critical · Error

plugin egress: ${hostname} resolves to a blocked address (${

Error message

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

What it means

Thrown by `resolveAndValidate` (plugins/_net.mjs:101) when DNS resolves a hostname to at least one address in a blocked range (loopback/private/link-local/metadata/CGNAT/ULA). Even one blocked address among the resolved set triggers the throw — this is strict SSRF/rebinding protection. The message names the offending address and flags 'possible SSRF/rebinding' because a hostname resolving to a public AND a private address is a classic DNS-rebinding signature. `allowsLocalhost` bypasses only loopback literals, nothing else.

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 9b17a8ac97)

Solutions

  1. Do not point plugins at hostnames that resolve to private/metadata ranges — reconfigure to a public endpoint.
  2. If the target is a genuine local service, use the loopback literal/hostname with `allowsLocalhost: true` in the provider manifest (this bypasses only loopback, not RFC1918/metadata).
  3. Investigate DNS rebinding if a domain flips between public and private IPs — pin the resolver or use a trusted upstream.
  4. Audit with `dig <hostname>` from the same host to see all resolved addresses.

Example fix

// resolveAndValidate rejects: example.internal → 10.0.0.5
// before — portals.yml
- name: internal
  url: https://example.internal/jobs
// after — expose a public endpoint, or route local AI through a loopback opt-in
- name: ollama
  provider: ollama
  base_url: http://127.0.0.1:11434
Defensive patterns

Strategy: validation

Validate before calling

import { lookup } from 'node:dns/promises';
import { isBlockedIp } from './plugins/_net.mjs';
// Detect DNS rebinding / private-resolution before the guarded fetch.
async function assertNoBlockedResolution(hostname) {
  const addrs = (await lookup(hostname, { all: true })).map(a => a.address);
  const blocked = addrs.filter(isBlockedIp);
  if (blocked.length) {
    throw new Error(`'${hostname}' resolves to blocked range ${blocked.join(',')} — possible rebinding; use a public endpoint.`);
  }
}
await assertNoBlockedResolution(hostname);

Try / catch

try {
  await resolveAndValidate(hostname);
} catch (err) {
  if (/possible SSRF\/rebinding/.test(err.message)) {
    // Security signal — never bypass; reconfigure to a public host.
    throw err;
  }
}

Prevention

When it happens

Trigger: A hostname resolves to a private/metadata IP (e.g. a rebinding domain that flips between public and 169.254.169.254), or to a mix of public and RFC1918 addresses. The loop over resolved addresses hits `isBlockedIp(address)` true and throws, after the loopback opt-in check.

Common situations: A DNS-rebinding attack domain; an internal hostname that legitimately resolves to a private IP but is being fetched by a plugin (not allowed); a provider whose DNS temporarily returns a private address due to a misconfigured record; split-horizon DNS where the internal view returns a private IP and the plugin runs inside that network.

Related errors


AI-assisted analysis of santifer/career-ops@9b17a8ac97 (2026-08-13). Data as JSON: /api/errors/0983afde2918d309. Report an issue: GitHub.