actualbudget/actual · warning

Blocked request to disallowed protocol: ${url.protocol}

Error message

Blocked request to disallowed protocol: ${url.protocol}

What it means

The SSRF guard restricts outbound requests made by the sync server to http: and https:. If the parsed URL uses any other scheme — file:, ftp:, gopher:, data:, etc. — it throws this error to prevent the server from being tricked into accessing non-HTTP resources or local files.

Source

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

 *
 * Pass { allowPrivateNetwork: true } for callers (e.g. SimpleFIN) whose
 * upstream may legitimately be a self-hosted server on the local network; the
 * always-blocked ranges (cloud metadata, reserved, broadcast) remain blocked
 * regardless.
 */
export async function assertUrlAllowed(
  targetUrl: string,
  options: SsrfOptions = {},
): Promise<void> {
  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 });

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Use only http:// or https:// URLs for the server/bootstrap endpoint configuration.
  2. If you intentionally serve over a custom scheme, switch that endpoint to HTTPS behind a proxy.
  3. If you are the server operator and this appears in logs, treat it as a malicious/probing request and review source IPs.
  4. Sanitize user-supplied URLs client-side to http(s) before sending.

Example fix

// before
const url = 'file:///etc/passwd';
// after
if (!/^https?:\/\//.test(url)) throw new Error('Only http(s) URLs are supported');
Defensive patterns

Strategy: validation

Validate before calling

function isHttpProtocol(value) {
  try {
    const u = new URL(value);
    return ['http:', 'https:'].includes(u.protocol);
  } catch {
    return false;
  }
}

Try / catch

try {
  await assertUrlAllowed(targetUrl);
} catch (e) {
  if (e.message.startsWith('Blocked request to disallowed protocol')) {
    logger.warn('Refused non-http(s) target:', targetUrl);
    return null;
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling claimAccessKey/getAccounts with a userFileUrl whose protocol is not http or https, e.g. 'file:///etc/passwd', 'ftp://host/file', or 'javascript:...' injected via request parameters.

Common situations: Attack payloads probing for SSRF/file-read via the bootstrapped/claim endpoints; misconfigured self-hosted setups pointing at non-HTTP internal services; template/env values accidentally containing a file: scheme.

Related errors


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