{"record":{"id":"5b7da9bd199fc8ab","repo":"actualbudget/actual","slug":"blocked-request-to-disallowed-protocol-url-prot","errorCode":null,"errorMessage":"Blocked request to disallowed protocol: ${url.protocol}","messagePattern":"Blocked request to disallowed protocol: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"packages/sync-server/src/util/ssrf.ts","lineNumber":84,"sourceCode":" *\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\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 });","sourceCodeStart":66,"sourceCodeEnd":102,"githubUrl":"https://github.com/actualbudget/actual/blob/d4334cb6e6123f4d3bcea1ad6166608884c7e658/packages/sync-server/src/util/ssrf.ts#L66-L102","documentation":"The SSRF guard restricts outbound requests made by the sync server to http: and https:. If the parsed URL uses any other scheme — file:, ftp:, gopher:, data:, etc. — it throws this error to prevent the server from being tricked into accessing non-HTTP resources or local files.","triggerScenarios":"Calling claimAccessKey/getAccounts with a userFileUrl whose protocol is not http or https, e.g. 'file:///etc/passwd', 'ftp://host/file', or 'javascript:...' injected via request parameters.","commonSituations":"Attack payloads probing for SSRF/file-read via the bootstrapped/claim endpoints; misconfigured self-hosted setups pointing at non-HTTP internal services; template/env values accidentally containing a file: scheme.","solutions":["Use only http:// or https:// URLs for the server/bootstrap endpoint configuration.","If you intentionally serve over a custom scheme, switch that endpoint to HTTPS behind a proxy.","If you are the server operator and this appears in logs, treat it as a malicious/probing request and review source IPs.","Sanitize user-supplied URLs client-side to http(s) before sending."],"exampleFix":"// before\nconst url = 'file:///etc/passwd';\n// after\nif (!/^https?:\\/\\//.test(url)) throw new Error('Only http(s) URLs are supported');","handlingStrategy":"validation","validationCode":"function isHttpProtocol(value) {\n  try {\n    const u = new URL(value);\n    return ['http:', 'https:'].includes(u.protocol);\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 disallowed protocol')) {\n    logger.warn('Refused non-http(s) target:', targetUrl);\n    return null;\n  }\n  throw e;\n}","preventionTips":["Only configure http/https endpoints for bootstrap/claim URLs.","Reject non-http(s) schemes in any user-facing URL input field.","Log blocked-protocol attempts and alert on repeats (likely probing)."],"tags":["ssrf","security","url","sync-server"],"backgroundTag":"blocked-url-protocol","analyzedSha":"d4334cb6e6123f4d3bcea1ad6166608884c7e658","analyzedAt":"2026-08-29T01:02:11.213Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}