{"record":{"id":"c3f0cef466e24d96","repo":"actualbudget/actual","slug":"url-not-allowed-unable-to-verify-allowlist","errorCode":null,"errorMessage":"URL not allowed: Unable to verify allowlist","messagePattern":"URL not allowed: Unable to verify allowlist","errorType":"http","errorClass":null,"httpStatus":403,"severity":"error","filePath":"packages/sync-server/src/app-cors-proxy.js","lineNumber":153,"sourceCode":"  // Validate session/token\n  const session = await validateSession(req, res);\n  if (!session) {\n    return; // validateSession already sent the response\n  }\n\n  let url;\n  try {\n    url = new URL(targetUrlString);\n  } catch {\n    return res.status(400).json({ error: 'Invalid url parameter' });\n  }\n\n  // Fetch the latest allowlist\n  try {\n    await fetchAllowlist();\n  } catch (error) {\n    console.error('Failed to fetch allowlist:', error);\n    return res.status(403).json({\n      error: 'URL not allowed',\n      message: 'Unable to verify allowlist',\n    });\n  }\n\n  // Check if the URL is allowed\n  if (!isUrlAllowed(url.href)) {\n    console.warn('Blocked request to unauthorized URL:', url.href);\n    return res.status(403).json({\n      error: 'URL not allowed',\n      message:\n        'Only allowlisted plugin repositories are allowed (localhost only in development)',\n    });\n  }\n\n  try {\n    const { method = 'GET', headers: customHeaders = {} } = req.body || {};\n","sourceCodeStart":135,"sourceCodeEnd":171,"githubUrl":"https://github.com/actualbudget/actual/blob/d4334cb6e6123f4d3bcea1ad6166608884c7e658/packages/sync-server/src/app-cors-proxy.js#L135-L171","documentation":"Before checking the target against allowlist rules, the proxy fetches the latest allowlist. If fetchAllowlist() throws (e.g. the allowlist source is unreachable), the proxy conservatively refuses the request with 403 'URL not allowed / Unable to verify allowlist'. This is a fail-closed behavior: inability to verify is treated as not allowed.","triggerScenarios":"The remote allowlist fetch fails — network outage on the server, allowlist host down, DNS failure, or invalid allowlist content — while a proxied request arrives.","commonSituations":"Self-hosted server without outbound internet access; allowlist URL misconfigured or moved; corporate firewall blocking the server's egress; transient upstream outage.","solutions":["Check the sync server's outbound network access and the console error logged server-side ('Failed to fetch allowlist') for the root cause.","Verify the allowlist source URL/configuration is correct and reachable (curl it from the server host).","Retry after the transient outage, or host a local allowlist copy so verification does not depend on an external service."],"exampleFix":"// before (server env with no egress)\nALLOWLIST_URL=https://plugins.actualbudget.org/allowlist.json\n// after: serve the allowlist locally\nALLOWLIST_URL=http://localhost:5006/allowlist.json","handlingStrategy":"retry","validationCode":"const allowlistUrl = process.env.ALLOWLIST_URL;\nif (allowlistUrl) {\n  const r = await fetch(allowlistUrl).catch(() => null);\n  if (!r || !r.ok) console.warn('Allowlist source unreachable; proxy requests will 403');\n}","typeGuard":null,"tryCatchPattern":"async function proxyWithRetry(url, attempts = 3) {\n  for (let i = 0; i < attempts; i++) {\n    const res = await fetch(`/cors-proxy?url=${encodeURIComponent(url)}`);\n    if (res.status !== 403) return res;\n    await new Promise(r => setTimeout(r, 2 ** i * 1000));\n  }\n  throw new Error('Proxy could not verify allowlist (403)');\n}","preventionTips":["Ensure the sync-server host has outbound internet access.","Host the allowlist locally if the server runs in a restricted network.","Monitor server logs for 'Failed to fetch allowlist' as an early warning."],"tags":["cors-proxy","network","allowlist","fail-closed"],"backgroundTag":"network-request-failed","analyzedSha":"d4334cb6e6123f4d3bcea1ad6166608884c7e658","analyzedAt":"2026-08-29T01:02:11.213Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}