{"record":{"id":"afff1f4db88f54b1","repo":"actualbudget/actual","slug":"blocked-request-to-host-resolving-to-private-local","errorCode":null,"errorMessage":"Blocked request to host resolving to private/local IP: ${hostname} (${address})","messagePattern":"Blocked request to host resolving to private/local IP: (.+?) \\((.+?)\\)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"packages/sync-server/src/util/ssrf.ts","lineNumber":113,"sourceCode":"    }\n    return;\n  }\n\n  // Hostname: resolve every address it points to and reject if any is blocked.\n  let addresses: { address: string }[];\n  try {\n    addresses = await dnsLookup(hostname, { all: true });\n  } catch {\n    throw new Error(`Unable to resolve host: ${hostname}`);\n  }\n\n  if (addresses.length === 0) {\n    throw new Error(`Unable to resolve host: ${hostname}`);\n  }\n\n  for (const { address } of addresses) {\n    if (isBlockedIp(address, options)) {\n      throw new Error(\n        `Blocked request to host resolving to private/local IP: ${hostname} (${address})`,\n      );\n    }\n  }\n}\n","sourceCodeStart":95,"sourceCodeEnd":119,"githubUrl":"https://github.com/actualbudget/actual/blob/d4334cb6e6123f4d3bcea1ad6166608884c7e658/packages/sync-server/src/util/ssrf.ts#L95-L119","documentation":"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).","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nconst url = 'http://localtest.me:5006'; // resolves to 127.0.0.1\n// after\nconst url = 'https://sync.mydomain.com'; // public addresses only\n// or, in dev, allow private ranges:\nawait assertUrlAllowed(url, { allowPrivateAddresses: true });","handlingStrategy":"validation","validationCode":"import { lookup } from 'dns/promises';\nimport ipaddr from 'ipaddr.js';\nexport async function resolvesOnlyPublic(hostname) {\n  const addrs = await lookup(hostname, { all: true });\n  return addrs.every(({ address }) => {\n    const r = ipaddr.parse(address).range();\n    return !['loopback','private','linkLocal','uniqueLocal'].includes(r);\n  });\n}","typeGuard":null,"tryCatchPattern":"try {\n  await assertUrlAllowed(targetUrl);\n} catch (e) {\n  if (e.message.startsWith('Blocked request to host resolving to private/local IP')) {\n    return { ok: false, reason: 'rebinding-or-internal-dns' };\n  }\n  throw e;\n}","preventionTips":["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."],"tags":["ssrf","security","dns","sync-server"],"backgroundTag":"ssrf-private-ip-blocked","analyzedSha":"d4334cb6e6123f4d3bcea1ad6166608884c7e658","analyzedAt":"2026-08-29T01:02:11.213Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}