{"record":{"id":"48274d632f42bbf1","repo":"actualbudget/actual","slug":"url-not-allowed-only-allowlisted-plugin-repositor","errorCode":null,"errorMessage":"URL not allowed: Only allowlisted plugin repositories are allowed (localhost only in development)","messagePattern":"URL not allowed: Only allowlisted plugin repositories are allowed \\(localhost only in development\\)","errorType":"http","errorClass":null,"httpStatus":403,"severity":"warning","filePath":"packages/sync-server/src/app-cors-proxy.js","lineNumber":162,"sourceCode":"  } 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\n    if (typeof method !== 'string') {\n      return res.status(400).json({ error: 'Invalid method parameter' });\n    }\n    const methodNormalized = method.toUpperCase();\n    if (!['GET', 'HEAD'].includes(methodNormalized)) {\n      return res.status(405).json({ error: 'Method not allowed' });\n    }\n\n    const requestHeaders = {","sourceCodeStart":144,"sourceCodeEnd":180,"githubUrl":"https://github.com/actualbudget/actual/blob/d4334cb6e6123f4d3bcea1ad6166608884c7e658/packages/sync-server/src/app-cors-proxy.js#L144-L180","documentation":"The CORS proxy only forwards requests to hosts on the plugin repository allowlist (localhost is additionally permitted in development). If the parsed target URL's href is not allowlisted, the proxy responds 403 with 'URL not allowed' and the allowlist-policy message. This is intentional security policy to stop the proxy being used as an open relay.","triggerScenarios":"A plugin requests an arbitrary API host (e.g. https://api.github.com or a personal CDN) that is not in the allowlist; a request to a non-localhost host while running a production server.","commonSituations":"Plugin author points fetches at their own repo/endpoint not yet on the allowlist; running localhost-only allowlisting in production by mistake; allowlist updated upstream but server hasn't refreshed it.","solutions":["Change the plugin to fetch only from allowlisted plugin repository hosts.","If the target repository is legitimate, get it added to the plugin allowlist (or configure a local allowlist) and restart the server.","In development, use a localhost URL, which the proxy permits."],"exampleFix":"// before\nfetch('/cors-proxy?url=' + encodeURIComponent('https://my-cdn.example.com/plugins.json'));\n// after: use an allowlisted repository\nfetch('/cors-proxy?url=' + encodeURIComponent('https://tantalus.life/plugins.json'));","handlingStrategy":"validation","validationCode":"const ALLOWED_HOSTS = ['tantalus.life', 'localhost'];\nconst u = new URL(target);\nif (!ALLOWED_HOSTS.includes(u.hostname)) {\n  throw new Error(`Host ${u.hostname} is not on the plugin allowlist`);\n}","typeGuard":"function isAllowlisted(target) {\n  try {\n    const u = new URL(target);\n    return ['tantalus.life', 'localhost'].includes(u.hostname);\n  } catch { return false; }\n}","tryCatchPattern":"try {\n  return await proxy(target);\n} catch (e) {\n  if (e.status === 403 && /allowlisted/i.test(e.message)) {\n    console.error(`Blocked by allowlist policy: ${target}`);\n  }\n  throw e;\n}","preventionTips":["Point plugins only at allowlisted plugin repository hosts.","Check the current allowlist before adding new remote sources to a plugin.","Use localhost targets during development where permitted."],"tags":["cors-proxy","security","allowlist","policy"],"backgroundTag":"url-blocked-by-allowlist","analyzedSha":"d4334cb6e6123f4d3bcea1ad6166608884c7e658","analyzedAt":"2026-08-29T01:02:11.213Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}