actualbudget/actual · error

Invalid URL

Error message

Invalid URL

What it means

assertUrlAllowed is the sync server's SSRF guard: it parses a caller-supplied URL and validates it before the server makes an outbound request. It throws 'Invalid URL' when the target string cannot be parsed by the URL constructor at all, so validation can proceed no further.

Source

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

 * Validate that a URL is safe to make a server-side request to, guarding
 * against SSRF. Only http(s) URLs are permitted, and the hostname is resolved
 * via DNS so that hostnames pointing at private/local/link-local addresses are
 * rejected as well as literal IPs. Throws if the URL is not allowed.
 *
 * 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;
  }

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Inspect the URL supplied by the client and confirm it is absolute and well-formed (scheme + host).
  2. Prepend the scheme if the user omitted it (e.g. turn 'example.com:5006' into 'https://example.com:5006').
  3. Trim whitespace and re-encode any characters illegal in URLs before sending.
  4. Validate the configured server URL with `new URL(value)` on the client side before sending it to the server.

Example fix

// before
await claimAccessKey('myserver:5006');
// after
const base = 'myserver:5006'.startsWith('http') ? 'myserver:5006' : 'https://myserver:5006';
await claimAccessKey(new URL(base).toString());
Defensive patterns

Strategy: validation

Validate before calling

function isValidHttpUrl(value) {
  try {
    const u = new URL(value);
    return u.protocol === 'http:' || u.protocol === 'https:';
  } catch {
    return false;
  }
}
// call only if isValidHttpUrl(targetUrl)

Try / catch

try {
  await assertUrlAllowed(targetUrl);
} catch (e) {
  if (e.message === 'Invalid URL') {
    throw new UserInputError('The server URL must be an absolute http(s) URL');
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling claimAccessKey or getAccounts (or any code path calling assertUrlAllowed) with a targetUrl that is empty, missing scheme, contains spaces/illegal characters, or is otherwise not an absolute URL — e.g. passing 'my-server.example' or a relative path instead of 'https://my-server.example'.

Common situations: Self-hosting misconfiguration where a base-URL env var is unset or truncated; users pasting a server address without the https:// scheme into client config; reverse proxies stripping the scheme when forwarding the GOAUTH/claim request.

Related errors


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