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

  1. Point the plugin/provider at a public hostname instead of a raw private/metadata IP.
  2. 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'.
  3. 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.
  4. 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

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


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