{"record":{"id":"0c9cdb7d5252c32e","repo":"actualbudget/actual","slug":"blocked-request-to-private-local-ip-hostname","errorCode":null,"errorMessage":"Blocked request to private/local IP: ${hostname}","messagePattern":"Blocked request to private/local IP: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"packages/sync-server/src/util/ssrf.ts","lineNumber":94,"sourceCode":"  let url: URL;\n  try {\n    url = new URL(targetUrl);\n  } catch {\n    throw new Error('Invalid URL');\n  }\n\n  if (url.protocol !== 'https:' && url.protocol !== 'http:') {\n    throw new Error(`Blocked request to disallowed protocol: ${url.protocol}`);\n  }\n\n  // URL keeps the surrounding brackets on IPv6 hosts (e.g. \"[::1]\"); strip\n  // them so the address can be parsed and resolved.\n  const hostname = url.hostname.replace(/^\\[|\\]$/g, '');\n\n  // Literal IP address: check it directly without a DNS lookup.\n  if (ipaddr.isValid(hostname)) {\n    if (isBlockedIp(hostname, options)) {\n      throw new Error(`Blocked request to private/local IP: ${hostname}`);\n    }\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)) {","sourceCodeStart":76,"sourceCodeEnd":112,"githubUrl":"https://github.com/actualbudget/actual/blob/d4334cb6e6123f4d3bcea1ad6166608884c7e658/packages/sync-server/src/util/ssrf.ts#L76-L112","documentation":"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).","triggerScenarios":"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/'.","commonSituations":"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.","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."],"exampleFix":"// before\nconst serverUrl = 'http://127.0.0.1:5006';\n// after\nconst serverUrl = 'https://sync.mydomain.com'; // publicly resolvable host","handlingStrategy":"validation","validationCode":"import ipaddr from 'ipaddr.js';\nfunction isPrivateLiteral(value) {\n  try {\n    const addr = ipaddr.parse(value);\n    const range = addr.range();\n    return ['loopback','private','linkLocal','uniqueLocal'].includes(range);\n  } catch {\n    return false;\n  }\n}","typeGuard":null,"tryCatchPattern":"try {\n  await assertUrlAllowed(targetUrl);\n} catch (e) {\n  if (e.message.startsWith('Blocked request to private/local IP')) {\n    return { allowed: false, reason: 'private-ip' };\n  }\n  throw e;\n}","preventionTips":["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."],"tags":["ssrf","security","network","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"}