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
- Inspect the URL supplied by the client and confirm it is absolute and well-formed (scheme + host).
- Prepend the scheme if the user omitted it (e.g. turn 'example.com:5006' into 'https://example.com:5006').
- Trim whitespace and re-encode any characters illegal in URLs before sending.
- 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
- Validate user-supplied server URLs with `new URL()` on the client before sending.
- Always include the scheme in configured base URLs.
- Trim whitespace from pasted URLs.
- Add a client-side form validation for the server address field.
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
- Blocked request to disallowed protocol: ${url.protocol}
- Invalid file ID
- Invalid group ID
- Invalid user IDs
- Invalid parameters
AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29).
Data as JSON: /api/errors/5ed5c373a64e5656.
Report an issue: GitHub.