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
- Do not point plugins at hostnames that resolve to private/metadata ranges — reconfigure to a public endpoint.
- 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).
- Investigate DNS rebinding if a domain flips between public and private IPs — pin the resolver or use a trusted upstream.
- 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
- Never fetch hostnames that resolve to private/metadata ranges from plugins.
- Pin DNS upstreams in environments prone to rebinding.
- Audit resolved addresses with `dig` from the runtime host.
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
- plugin egress to ${hostname} is blocked (private/loopback/me
- plugin egress: cannot resolve ${hostname} — ${err.message}
- plugin egress: ${hostname} resolved to no addresses
- DNS resolution returned no addresses for ${hostname}
- Access denied: Egress guard blocked private target IP ${ip}
AI-assisted analysis of santifer/career-ops@9b17a8ac97 (2026-08-13).
Data as JSON: /api/errors/0983afde2918d309.
Report an issue: GitHub.