ruvnet/ruflo · error · HttpFetchValidationError

PRIVATE_ADDRESS

PRIVATE_ADDRESS

Error message

host ${host} is loopback/private/link-local; set CLAUDE_FLOW_HTTP_FETCH_ALLOW_PRIVATE=1 to override

What it means

Thrown by validateUrl as an SSRF guard: the parsed hostname resolves to loopback, RFC-1918 private space, link-local, CGNAT, or IPv6 ULA/loopback, and the env override CLAUDE_FLOW_HTTP_FETCH_ALLOW_PRIVATE is not set to '1'. This secure-by-default posture stops the fetch tool from reaching internal services. The error carries code PRIVATE_ADDRESS.

Source

Thrown at v3/@claude-flow/cli/src/mcp-tools/http-fetch-tools.ts:63

 */
export function validateUrl(rawUrl: string): URL {
  let parsed: URL;
  try {
    parsed = new URL(rawUrl);
  } catch {
    throw new HttpFetchValidationError(`invalid URL: ${rawUrl}`, 'INVALID_URL');
  }
  const proto = parsed.protocol.toLowerCase();
  if (proto !== 'http:' && proto !== 'https:') {
    throw new HttpFetchValidationError(
      `protocol ${parsed.protocol} not allowed (only http: and https:)`,
      'FORBIDDEN_PROTOCOL',
    );
  }
  const host = parsed.hostname.toLowerCase();
  const allowPrivate = process.env.CLAUDE_FLOW_HTTP_FETCH_ALLOW_PRIVATE === '1';
  if (!allowPrivate && isPrivateOrLoopback(host)) {
    throw new HttpFetchValidationError(
      `host ${host} is loopback/private/link-local; set CLAUDE_FLOW_HTTP_FETCH_ALLOW_PRIVATE=1 to override`,
      'PRIVATE_ADDRESS',
    );
  }
  return parsed;
}

function isPrivateOrLoopback(host: string): boolean {
  if (host === 'localhost' || host === 'localhost.localdomain') return true;
  // IPv6 loopback
  if (host === '::1' || host === '[::1]') return true;
  // IPv4 numeric checks
  const m = host.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
  if (m) {
    const a = Number(m[1]);
    const b = Number(m[2]);
    if (a === 0) return true;          // 0.0.0.0/8
    if (a === 127) return true;        // loopback

View on GitHub (pinned to 6b01dc5a68)

Solutions

  1. Set CLAUDE_FLOW_HTTP_FETCH_ALLOW_PRIVATE=1 in the environment when fetching local/internal targets is intentional.
  2. Route the fetch through a public-facing endpoint (tunnel, ingress) instead of the private address.
  3. Confirm you actually need a private target — the default is a security control, not a bug.
  4. For one-off local testing: `CLAUDE_FLOW_HTTP_FETCH_ALLOW_PRIVATE=1 npx @claude-flow/cli ...`.

Example fix

// before
http_fetch({ url: 'http://localhost:3000/health' })  // throws PRIVATE_ADDRESS
// after
// set env: export CLAUDE_FLOW_HTTP_FETCH_ALLOW_PRIVATE=1
http_fetch({ url: 'http://localhost:3000/health' })
Defensive patterns

Strategy: validation

Validate before calling

function assertPublicUrl(raw, allowPrivate = process.env.CLAUDE_FLOW_HTTP_FETCH_ALLOW_PRIVATE === '1') {
  const u = new URL(raw);
  const host = u.hostname.toLowerCase();
  if (!allowPrivate && isPrivate(host)) {
    throw new Error(`private host ${host}; set CLAUDE_FLOW_HTTP_FETCH_ALLOW_PRIVATE=1`);
  }
  return u;
}

Prevention

When it happens

Trigger: URLs targeting localhost, 127.0.0.1, ::1, 10.x, 172.16-31.x, 192.168.x, 169.254.x, 100.64-127.x (CGNAT), fc/fd (IPv6 ULA), fe80: (link-local), or 0.0.0.0 — without CLAUDE_FLOW_HTTP_FETCH_ALLOW_PRIVATE=1 in the environment.

Common situations: Fetching a local dev server (http://localhost:3000); hitting an internal service on a private subnet; cloud metadata endpoint 169.254.169.254; testing against a Docker bridge IP; CI environments where the target is on a private network.

Related errors


AI-assisted analysis of ruvnet/ruflo@6b01dc5a68 (2026-08-12). Data as JSON: /api/errors/0d68f30a14aa785f. Report an issue: GitHub.