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
- Use a public DNS hostname for the target server instead of a raw private IP.
- 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.
- Access the sync server through its public domain rather than internal addresses.
- 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
- Use public hostnames for sync server endpoints.
- Do not aim client configs at 127.0.0.1/10.x/192.168.x addresses when going through the SSRF-checked path.
- For local development, use an allowlisted options override rather than raw private IPs.
- Monitor logs for repeated private-IP probes.
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
- Blocked request to disallowed protocol: ${url.protocol}
- Unable to resolve host: ${hostname}
- Blocked request to host resolving to private/local IP: ${hos
- No sync server configured.
- getServerErrorReason(json)
AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29).
Data as JSON: /api/errors/0c9cdb7d5252c32e.
Report an issue: GitHub.