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
- 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).
- 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).
- Expose the internal service through an approved public gateway/proxy instead of addressing it by internal DNS.
- 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
- Never point plugin egress at internal, loopback, or cloud-metadata hostnames.
- Enable the localhost allowance only when you intentionally talk to local services.
- Audit configured base URLs for hosts that resolve to private ranges (CI often differs from laptops).
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
- Access denied: Egress guard blocked private target IP
- Blocked request to restricted destination (DNS)
- plugin egress: resolved to no addresses
- Access denied: Egress guard blocked private target IP
- Access denied: Egress guard blocked private target IPv6
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)