actualbudget/actual · warning
Blocked request to host resolving to private/local IP: ${hos
Error message
Blocked request to host resolving to private/local IP: ${hostname} (${address}) What it means
After resolving a hostname, the SSRF guard checks every returned address with isBlockedIp and rejects the request if ANY address falls in a private/loopback/link-local range. This defeats DNS-rebinding and hostname-to-internal-IP tricks where a public name points at internal infrastructure (e.g. 'localtest.me' -> 127.0.0.1, or rebinding to 169.254.169.254).
Source
Thrown at packages/sync-server/src/util/ssrf.ts:113
}
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)) {
throw new Error(
`Blocked request to host resolving to private/local IP: ${hostname} (${address})`,
);
}
}
}
View on GitHub (pinned to d4334cb6e6)
Solutions
- Point the client at a hostname that resolves only to public addresses.
- For local development, use the SSRF guard's permissive options (allow private ranges) instead of a wildcard-DNS-to-loopback trick.
- If operating legitimately on an internal network, add an allowlist entry in SsrfOptions for the specific internal range/host.
- If seen unexpectedly in logs, treat as an SSRF/rebinding probe and block the client.
Example fix
// before
const url = 'http://localtest.me:5006'; // resolves to 127.0.0.1
// after
const url = 'https://sync.mydomain.com'; // public addresses only
// or, in dev, allow private ranges:
await assertUrlAllowed(url, { allowPrivateAddresses: true }); Defensive patterns
Strategy: validation
Validate before calling
import { lookup } from 'dns/promises';
import ipaddr from 'ipaddr.js';
export async function resolvesOnlyPublic(hostname) {
const addrs = await lookup(hostname, { all: true });
return addrs.every(({ address }) => {
const r = ipaddr.parse(address).range();
return !['loopback','private','linkLocal','uniqueLocal'].includes(r);
});
} Try / catch
try {
await assertUrlAllowed(targetUrl);
} catch (e) {
if (e.message.startsWith('Blocked request to host resolving to private/local IP')) {
return { ok: false, reason: 'rebinding-or-internal-dns' };
}
throw e;
} Prevention
- Point clients at hostnames that resolve exclusively to public IPs.
- Avoid wildcard-DNS-to-loopback services (localtest.me, nip.io) in server-side requests.
- Use explicit permissive SsrfOptions allowlists for legitimate internal hosts instead of internal DNS names.
- Alert on rebinding-pattern requests in logs.
When it happens
Trigger: Calling claimAccessKey/getAccounts with a public-looking hostname whose DNS A records include any private/loopback/metadata address — e.g. 'http://localtest.me/' (resolves to 127.0.0.1), a rebinding domain, or an internal name like 'sync.lan' that resolves to 10.0.0.5.
Common situations: DNS rebinding attack attempts against the sync server; self-hosted setups using split-horizon DNS where internal names resolve to private IPs; wildcard DNS services mapping names to 127.0.0.1 used for local testing.
Related errors
- Blocked request to disallowed protocol: ${url.protocol}
- Blocked request to private/local IP: ${hostname}
- Unable to resolve host: ${hostname}
- Invalid URL
- No id returned from download.
AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29).
Data as JSON: /api/errors/afff1f4db88f54b1.
Report an issue: GitHub.