actualbudget/actual · warning

URL not allowed: Only allowlisted plugin repositories are al

Error message

URL not allowed: Only allowlisted plugin repositories are allowed (localhost only in development)

What it means

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.

Source

Thrown at packages/sync-server/src/app-cors-proxy.js:162

  } catch {
    return res.status(400).json({ error: 'Invalid url parameter' });
  }

  // Fetch the latest allowlist
  try {
    await fetchAllowlist();
  } catch (error) {
    console.error('Failed to fetch allowlist:', error);
    return res.status(403).json({
      error: 'URL not allowed',
      message: 'Unable to verify allowlist',
    });
  }

  // Check if the URL is allowed
  if (!isUrlAllowed(url.href)) {
    console.warn('Blocked request to unauthorized URL:', url.href);
    return res.status(403).json({
      error: 'URL not allowed',
      message:
        'Only allowlisted plugin repositories are allowed (localhost only in development)',
    });
  }

  try {
    const { method = 'GET', headers: customHeaders = {} } = req.body || {};

    if (typeof method !== 'string') {
      return res.status(400).json({ error: 'Invalid method parameter' });
    }
    const methodNormalized = method.toUpperCase();
    if (!['GET', 'HEAD'].includes(methodNormalized)) {
      return res.status(405).json({ error: 'Method not allowed' });
    }

    const requestHeaders = {

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Change the plugin to fetch only from allowlisted plugin repository hosts.
  2. If the target repository is legitimate, get it added to the plugin allowlist (or configure a local allowlist) and restart the server.
  3. In development, use a localhost URL, which the proxy permits.

Example fix

// before
fetch('/cors-proxy?url=' + encodeURIComponent('https://my-cdn.example.com/plugins.json'));
// after: use an allowlisted repository
fetch('/cors-proxy?url=' + encodeURIComponent('https://tantalus.life/plugins.json'));
Defensive patterns

Strategy: validation

Validate before calling

const ALLOWED_HOSTS = ['tantalus.life', 'localhost'];
const u = new URL(target);
if (!ALLOWED_HOSTS.includes(u.hostname)) {
  throw new Error(`Host ${u.hostname} is not on the plugin allowlist`);
}

Type guard

function isAllowlisted(target) {
  try {
    const u = new URL(target);
    return ['tantalus.life', 'localhost'].includes(u.hostname);
  } catch { return false; }
}

Try / catch

try {
  return await proxy(target);
} catch (e) {
  if (e.status === 403 && /allowlisted/i.test(e.message)) {
    console.error(`Blocked by allowlist policy: ${target}`);
  }
  throw e;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29). Data as JSON: /api/errors/48274d632f42bbf1. Report an issue: GitHub.