santifer/career-ops · error · Error

plugin egress to is blocked (private/loopback/metadata…

Error message

plugin egress to ${hostname} is blocked (private/loopback/metadata range)

What it means

resolveAndValidate in the plugin egress guard resolves a hostname to IP addresses and blocks any address in private, loopback, link-local, or cloud-metadata ranges. For a hostname that is already an IP literal, it validates directly via isBlockedIp and throws this error when the IP falls in a blocked range. The guard exists to stop SSRF: plugins must not be able to reach internal services, localhost, or metadata endpoints (e.g. 169.254.169.254).

Solutions

  1. If the target is a legitimate local AI provider (Ollama/LM Studio), pass { allowsLocalhost: true } and use a loopback literal (127.0.0.1) — private non-loopback IPs stay blocked.
  2. Change the plugin's endpoint to a public hostname/IP; the block is intentional for anything private.
  3. If the endpoint must be internal, move it behind an approved public gateway/proxy rather than weakening the guard.
  4. Check the hostname value in the plugin config for typos (0.0.0.0, 169.254.x, LAN addresses).

Example fix

// before: localhost target blocked because allowsLocalhost not passed
await resolveAndValidate("127.0.0.1:11434");

// after: explicitly allow loopback for local providers
await resolveAndValidate("127.0.0.1", { allowsLocalhost: true });
Defensive patterns

Strategy: validation

Validate before calling

import { isIP } from 'net';
const BLOCKED_RE = /^(10\.|127\.|169\.254\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.|0\.0\.0\.0$)/;
function isPublicEndpoint(hostname) {
  const host = hostname.replace(/:\d+$/, '');
  if (isIP(host)) return !BLOCKED_RE.test(host);
  return !['localhost', 'metadata.google.internal'].includes(host.toLowerCase());
}
// before calling: isPublicEndpoint(hostname) || explicitlyPassingAllowsLocalhost

Type guard

function isLoopbackLiteral(host) {
  return isIP(host) !== 0 && /^(127\.|::1$)/.test(host);
}

Try / catch

try {
  await resolveAndValidate(hostname, { allowsLocalhost });
} catch (err) {
  if (err.message.includes('is blocked (private/loopback/metadata range)')) {
    console.error(`Endpoint ${hostname} is private/metadata — use a public endpoint or allowsLocalhost with 127.0.0.1`);
  }
  throw err;
}

Prevention

When it happens

Trigger: Calling resolveAndValidate with (a) an IP-literal hostname in a private range (10.x, 192.168.x, 127.x, 169.254.x, etc.) without allowsLocalhost, or (b) such an IP with allowsLocalhost=true where the IP is private/metadata but not a loopback literal (the loopback exemption only covers 127.0.0.0/8-style literals).

Common situations: A plugin configured to call a local Ollama/LM Studio instance at 127.0.0.1 but the caller forgot to pass allowsLocalhost: true; a plugin pointed at an internal staging service on 10.x; a config using a metadata IP or a LAN address (192.168.1.x) as an API endpoint; typo'd base URL like http://0.0.0.0.

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


AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16). Data as JSON: /api/errors/81844cb0f7866812. Report an issue: GitHub.

Appendix: 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 aac998c7ed)