{"record":{"id":"5ed5c373a64e5656","repo":"actualbudget/actual","slug":"invalid-url","errorCode":null,"errorMessage":"Invalid URL","messagePattern":"Invalid URL","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/sync-server/src/util/ssrf.ts","lineNumber":80,"sourceCode":" * Validate that a URL is safe to make a server-side request to, guarding\n * against SSRF. Only http(s) URLs are permitted, and the hostname is resolved\n * via DNS so that hostnames pointing at private/local/link-local addresses are\n * rejected as well as literal IPs. Throws if the URL is not allowed.\n *\n * Pass { allowPrivateNetwork: true } for callers (e.g. SimpleFIN) whose\n * upstream may legitimately be a self-hosted server on the local network; the\n * always-blocked ranges (cloud metadata, reserved, broadcast) remain blocked\n * regardless.\n */\nexport async function assertUrlAllowed(\n  targetUrl: string,\n  options: SsrfOptions = {},\n): Promise<void> {\n  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","sourceCodeStart":62,"sourceCodeEnd":98,"githubUrl":"https://github.com/actualbudget/actual/blob/d4334cb6e6123f4d3bcea1ad6166608884c7e658/packages/sync-server/src/util/ssrf.ts#L62-L98","documentation":"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.","triggerScenarios":"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'.","commonSituations":"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.","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."],"exampleFix":"// before\nawait claimAccessKey('myserver:5006');\n// after\nconst base = 'myserver:5006'.startsWith('http') ? 'myserver:5006' : 'https://myserver:5006';\nawait claimAccessKey(new URL(base).toString());","handlingStrategy":"validation","validationCode":"function isValidHttpUrl(value) {\n  try {\n    const u = new URL(value);\n    return u.protocol === 'http:' || u.protocol === 'https:';\n  } catch {\n    return false;\n  }\n}\n// call only if isValidHttpUrl(targetUrl)","typeGuard":null,"tryCatchPattern":"try {\n  await assertUrlAllowed(targetUrl);\n} catch (e) {\n  if (e.message === 'Invalid URL') {\n    throw new UserInputError('The server URL must be an absolute http(s) URL');\n  }\n  throw e;\n}","preventionTips":["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."],"tags":["ssrf","url","validation","sync-server"],"backgroundTag":"invalid-url","analyzedSha":"d4334cb6e6123f4d3bcea1ad6166608884c7e658","analyzedAt":"2026-08-29T01:02:11.213Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}