actualbudget/actual · warning

Blocked request to private/local IP: ${hostname}

Error message

Blocked request to private/local IP: ${hostname}

What it means

As part of SSRF protection, the sync server blocks URLs whose host is a literal IP address in private, loopback, link-local, or otherwise local ranges (via ipaddr + isBlockedIp). This prevents requests to internal network resources such as 127.0.0.1, 10.x, 192.168.x, or cloud metadata endpoints (169.254.169.254).

Source

Thrown at packages/sync-server/src/util/ssrf.ts:94

  let url: URL;
  try {
    url = new URL(targetUrl);
  } catch {
    throw new Error('Invalid URL');
  }

  if (url.protocol !== 'https:' && url.protocol !== 'http:') {
    throw new Error(`Blocked request to disallowed protocol: ${url.protocol}`);
  }

  // URL keeps the surrounding brackets on IPv6 hosts (e.g. "[::1]"); strip
  // them so the address can be parsed and resolved.
  const hostname = url.hostname.replace(/^\[|\]$/g, '');

  // Literal IP address: check it directly without a DNS lookup.
  if (ipaddr.isValid(hostname)) {
    if (isBlockedIp(hostname, options)) {
      throw new Error(`Blocked request to private/local IP: ${hostname}`);
    }
    return;
  }

  // Hostname: resolve every address it points to and reject if any is blocked.
  let addresses: { address: string }[];
  try {
    addresses = await dnsLookup(hostname, { all: true });
  } catch {
    throw new Error(`Unable to resolve host: ${hostname}`);
  }

  if (addresses.length === 0) {
    throw new Error(`Unable to resolve host: ${hostname}`);
  }

  for (const { address } of addresses) {
    if (isBlockedIp(address, options)) {

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Use a public DNS hostname for the target server instead of a raw private IP.
  2. For local development, run the URL check with permissive SsrfOptions that allow private ranges (the guard supports an options override) or bypass the SSRF check in dev config.
  3. Access the sync server through its public domain rather than internal addresses.
  4. If this is an unexpected log entry, treat it as an SSRF probe and block the client IP.

Example fix

// before
const serverUrl = 'http://127.0.0.1:5006';
// after
const serverUrl = 'https://sync.mydomain.com'; // publicly resolvable host
Defensive patterns

Strategy: validation

Validate before calling

import ipaddr from 'ipaddr.js';
function isPrivateLiteral(value) {
  try {
    const addr = ipaddr.parse(value);
    const range = addr.range();
    return ['loopback','private','linkLocal','uniqueLocal'].includes(range);
  } catch {
    return false;
  }
}

Try / catch

try {
  await assertUrlAllowed(targetUrl);
} catch (e) {
  if (e.message.startsWith('Blocked request to private/local IP')) {
    return { allowed: false, reason: 'private-ip' };
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling claimAccessKey/getAccounts with a URL whose hostname is directly a blocked literal IP, e.g. 'http://127.0.0.1:5006', 'http://192.168.1.10/', or 'http://169.254.169.254/latest/meta-data/'.

Common situations: Developers pointing the client at a localhost sync server while the SSRF guard is enabled; SSRF probes against internal networks or cloud metadata endpoints; docker-compose setups where the configured server URL is a container-local IP.

Related errors


AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29). Data as JSON: /api/errors/0c9cdb7d5252c32e. Report an issue: GitHub.