santifer/career-ops · critical · Error
plugin egress to ${hostname} is blocked (private/loopback/me
Error message
plugin egress to ${hostname} is blocked (private/loopback/metadata range) What it means
Thrown by `resolveAndValidate` (plugins/_net.mjs:81) when the destination is an IP literal (no DNS needed) and that literal falls in a blocked range. Blocked ranges include loopback (127/8, ::1), RFC1918 private (10/8, 172.16/12, 192.168/16), link-local + cloud metadata (169.254/16, incl. 169.254.169.254), CGNAT (100.64/10), unspecified, and IPv4-mapped-IPv6 variants. This is SSRF egress protection: a plugin's fetch is refused before it reaches an internal/metadata target. The `allowsLocalhost` opt-in bypasses only loopback literals, not private/metadata.
Source
Thrown at plugins/_net.mjs:81
if (a >= 224) return true; // multicast / reserved
return false;
}
const LOOPBACK_HOSTS = new Set(['localhost', '127.0.0.1', '::1', '[::1]']);
/**
* Resolve a hostname and reject if ANY resolved address is blocked. Returns the
* validated addresses. Throws on a blocked or unresolvable host.
* @param {string} hostname
* @param {{ allowsLocalhost?: boolean }} [opts]
* @returns {Promise<string[]>}
*/
export async function resolveAndValidate(hostname, { allowsLocalhost = false } = {}) {
// An IP literal host: validate directly (no DNS).
if (isIP(hostname)) {
if (isBlockedIp(hostname)) {
if (allowsLocalhost && isLoopbackLiteral(hostname)) return [hostname];
throw new Error(`plugin egress to ${hostname} is blocked (private/loopback/metadata range)`);
}
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)) {View on GitHub (pinned to 9b17a8ac97)
Solutions
- Point the plugin/provider at a public hostname instead of a raw private/metadata IP.
- If the target is genuinely a local service (e.g. Ollama at 127.0.0.1), set the provider manifest's `allowsLocalhost: true` and use the loopback literal or 'localhost'.
- Never target link-local (169.254.x) or RFC1918 ranges from a plugin — these are always blocked for SSRF safety; route through an allowed public endpoint instead.
- Audit portals.yml and any plugin-supplied URLs for raw IP literals.
Example fix
// before — portals.yml - name: internal-jobs url: http://10.0.0.5/api/jobs # RFC1918 → blocked // after — use the public hostname, or opt into localhost for a local AI provider - name: ollama provider: ollama base_url: http://127.0.0.1:11434 # allowed because manifest sets allowsLocalhost:true
Defensive patterns
Strategy: validation
Validate before calling
import { isBlockedIp } from './plugins/_net.mjs';
import { isIP } from 'node:net';
// Reject raw private/metadata IPs in config before they reach egress validation.
function assertPublicIpLiteral(host) {
if (isIP(host) && isBlockedIp(host)) {
throw new Error(`Refusing private/metadata IP literal in config: ${host}`);
}
}
for (const entry of portals) assertPublicIpLiteral(new URL(entry.url).hostname); Try / catch
try {
await resolveAndValidate(hostname, { allowsLocalhost: manifest.allowsLocalhost });
} catch (err) {
if (/blocked \(private\/loopback\/metadata/.test(err.message)) {
console.error(`SSRF guard rejected target: ${err.message}`);
// do not retry; this is a config/security issue
throw err;
}
} Prevention
- Never put raw private/metadata IPs in plugin URLs.
- Set `allowsLocalhost: true` only for genuine local-AI providers, and only loopback is then permitted.
When it happens
Trigger: A plugin entry (portals.yml) or its fetch target resolves to a raw IP like `http://10.0.0.5/...`, `http://169.254.169.254/latest/meta-data/`, or `http://192.168.1.1`. Since the host is already an IP, resolveAndValidate validates it directly and throws because it is in a blocked range, unless `allowsLocalhost` is set AND it is a loopback literal.
Common situations: A misconfigured plugin pointing at an internal service IP; an attempt (benign or hostile) to reach the AWS/GCP/Azure metadata endpoint via a plugin; a plugin targeting a private network resource during local dev without the localhost opt-in; a provider whose base URL was mistakenly set to a private IP.
Related errors
- plugin egress: ${hostname} resolves to a blocked address (${
- Access denied: Egress guard blocked private target IP ${ip}
- Refusing non-HTTP(S) URL: ${url}
- Refusing private/loopback host: ${host}
- plugin egress must use HTTPS: ${u.href}
AI-assisted analysis of santifer/career-ops@9b17a8ac97 (2026-08-13).
Data as JSON: /api/errors/81844cb0f7866812.
Report an issue: GitHub.