Every CVE Search/ Get API key

CVE-2026-57449

Actual Sync Server: CORS Proxy GitHub API Allowlist Prefix Bypass Leaks Private Repositories Through the Server GitHub Token

7.1 HighFix available

Actual is a local-first personal finance tool. Prior to 26.7.0, Actual Sync Server's CORS proxy is intended to let authenticated users fetch resources only from repositories listed in the official plugin allowlist. When ACTUAL_GITHUB_TOKEN is configured, the proxy automatically attaches the server's GitHub token to GitHub requests. The GitHub API allowlist check uses a raw startsWith() prefix test for /repos/{owner}/{repo} without requiring a path boundary after the repository name. If an allowlisted public plugin repository is https://github.com/acme/plugin, the proxy also accepts GitHub API URLs. Those URLs are outside the allowlisted repository but still pass because their API path starts with /repos/acme/plugin. The proxy then forwards the request with the server's ACTUAL_GITHUB_TOKEN, allowing any authenticated Actual user to read private GitHub resources reachable by that token. Version 26.7.0 fixes the issue.

Attack profile

How easy it is to exploit

Attack vector
Network
Complexity
Low
Privileges needed
Low
User interaction
None

What an attacker can affect

Confidentiality
High
Integrity
None
Availability
None

Exploit likelihood

0.26%Up 0.1 points since Sep 26, 2026
90%50%10%1%

What each source reports

CVE List Score used 7.1

Actual Sync Server: CORS Proxy GitHub API Allowlist Prefix Bypass Leaks Private Repositories Through the Server GitHub Token

Actual is a local-first personal finance tool. Prior to 26.7.0, Actual Sync Server's CORS proxy is intended to let authenticated users fetch resources only from repositories listed in the official plugin allowlist. When ACTUAL_GITHUB_TOKEN is configured, the proxy automatically attaches the server's GitHub token to GitHub requests. The GitHub API allowlist check uses a raw startsWith() prefix test for /repos/{owner}/{repo} without requiring a path boundary after the repository name. If an allowlisted public plugin repository is https://github.com/acme/plugin, the proxy also accepts GitHub API URLs. Those URLs are outside the allowlisted repository but still pass because their API path starts with /repos/acme/plugin. The proxy then forwards the request with the server's ACTUAL_GITHUB_TOKEN, allowing any authenticated Actual user to read private GitHub resources reachable by that token. Version 26.7.0 fixes the issue.

Record
CVE-2026-57449
CVSS 4.0
7.1 HighCVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N
Weakness
CWE-200, CWE-284, CWE-863
Updated
2026-11-02
EUVDEUVD-2026-87369 7.1

Actual is a local-first personal finance tool. Prior to 26.7.0, Actual Sync Server's CORS proxy is intended to let authenticated users fetch resources only from repositories listed in the official plugin allowlist. When ACTUAL_GITHUB_TOKEN is configured, the proxy automatically attaches the server's GitHub token to GitHub requests. The GitHub API allowlist check uses a raw startsWith() prefix test for /repos/{owner}/{repo} without requiring a path boundary after the repository name. If an allowlisted public plugin repository is https://github.com/acme/plugin, the proxy also accepts GitHub API URLs. Those URLs are outside the allowlisted repository but still pass because their API path starts with /repos/acme/plugin. The proxy then forwards the request with the server's ACTUAL_GITHUB_TOKEN, allowing any authenticated Actual user to read private GitHub resources reachable by that token. Version 26.7.0 fixes the issue.

Record
EUVD-2026-87369
CVSS 4.0
7.1 HighCVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N
Updated
2026-11-02
GitHub AdvisoriesGHSA-m62c-5q34-f3cf 7.1

Actual Sync Server: CORS Proxy GitHub API Allowlist Prefix Bypass Leaks Private Repositories Through the Server GitHub Token

Summary

Actual Sync Server's CORS proxy is intended to let authenticated users fetch resources only from repositories listed in the official plugin allowlist. When ACTUAL_GITHUB_TOKEN is configured, the proxy automatically attaches the server's GitHub token to GitHub requests.

The GitHub API allowlist check uses a raw startsWith() prefix test for /repos/{owner}/{repo} without requiring a path boundary after the repository name. If an allowlisted public plugin repository is https://github.com/acme/plugin, the proxy also accepts GitHub API URLs such as:

https://api.github.com/repos/acme/plugin-private/contents/.env
https://api.github.com/repos/acme/plugin-secrets/actions/secrets
https://api.github.com/repos/acme/plugin-internal/releases

Those URLs are outside the allowlisted repository but still pass because their API path starts with /repos/acme/plugin. The proxy then forwards the request with the server's ACTUAL_GITHUB_TOKEN, allowing any authenticated Actual user to read private GitHub resources reachable by that token.

Affected Endpoint

  • GET /cors-proxy?url=...

This endpoint is mounted only when:

if (config.get('corsProxy.enabled')) {
  app.use('/cors-proxy', corsApp.handlers);
}

Source: packages/sync-server/src/app.ts:68-70

Details

The CORS proxy is disabled by default but can be enabled through ACTUAL_CORS_PROXY_ENABLED. The GitHub token is configured through ACTUAL_GITHUB_TOKEN:

github: {
  token: {
    doc: 'GitHub Personal Access Token for API authentication.',
    format: String,
    default: '',
    env: 'ACTUAL_GITHUB_TOKEN',
  },
},
corsProxy: {
  enabled: {
    doc: 'Enable the CORS proxy endpoint.',
    format: Boolean,
    default: false,
    env: 'ACTUAL_CORS_PROXY_ENABLED',
  },
},

Source: packages/sync-server/src/load-config.js:280-296

The proxy fetches the plugin allowlist and stores repository URLs:

const response = await fetch(
  'https://raw.githubusercontent.com/actualbudget/plugin-store/refs/heads/main/plugins.json',
);
...
const plugins = await response.json();
allowlistedRepos = plugins.map(plugin => plugin.url);

Source: packages/sync-server/src/app-cors-proxy.js:43-50

The GitHub API check accepts any path that starts with /repos/${repoOwner}/${repoName}:

for (const repoUrl of allowlistedRepos) {
  const { pathname } = new URL(repoUrl);
  const [, repoOwner, repoName] = pathname.split('/');

  if (
    targetUrl === repoUrl ||
    targetUrl.startsWith(repoUrl + '/') ||
    (hostname === 'api.github.com' &&
      url.pathname.startsWith(`/repos/${repoOwner}/${repoName}`)) ||
    ...
  ) {
    return true;
  }
}

Source: packages/sync-server/src/app-cors-proxy.js:84-99

For api.github.com, there is no delimiter after repoName. This means an allowlisted repo named plugin authorizes API requests for plugin-private, plugin-secrets, plugin-internal, and any other repository under the same owner whose name starts with plugin.

After this incorrect allowlist decision, the proxy attaches the server's GitHub token:

const githubToken = config.get('github.token');
if (
  githubToken &&
  (url.hostname === 'api.github.com' ||
    url.hostname === 'raw.githubusercontent.com' ||
    (url.hostname === 'github.com' && url.pathname.includes('/releases/')))
) {
  requestHeaders['Authorization'] = `Bearer ${githubToken}`;
  requestHeaders['User-Agent'] = 'Actual-Budget-Plugin-System';
}

Source: packages/sync-server/src/app-cors-proxy.js:192-201

Therefore the vulnerable flow is:

  1. A public plugin repository is allowlisted, for example https://github.com/acme/plugin.
  2. The same owner has a private repository with a prefix-matching name, for example acme/plugin-private.
  3. The Actual server has ACTUAL_CORS_PROXY_ENABLED=true.
  4. The Actual server has ACTUAL_GITHUB_TOKEN with access to acme/plugin-private.
  5. Any authenticated Actual user calls:
Record
GHSA-m62c-5q34-f3cf
CVSS 4.0
7.1 HighCVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N
Weakness
CWE-200, CWE-284, CWE-863
Updated
2026-11-02
NVD 7.1

Actual is a local-first personal finance tool. Prior to 26.7.0, Actual Sync Server's CORS proxy is intended to let authenticated users fetch resources only from repositories listed in the official plugin allowlist. When ACTUAL_GITHUB_TOKEN is configured, the proxy automatically attaches the server's GitHub token to GitHub requests. The GitHub API allowlist check uses a raw startsWith() prefix test for /repos/{owner}/{repo} without requiring a path boundary after the repository name. If an allowlisted public plugin repository is https://github.com/acme/plugin, the proxy also accepts GitHub API URLs. Those URLs are outside the allowlisted repository but still pass because their API path starts with /repos/acme/plugin. The proxy then forwards the request with the server's ACTUAL_GITHUB_TOKEN, allowing any authenticated Actual user to read private GitHub resources reachable by that token. Version 26.7.0 fixes the issue.

Record
CVE-2026-57449
CVSS 4.0
7.1 HighCVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X
Weakness
CWE-200, CWE-284, CWE-863
Updated
2026-11-02
OSV 7.1

Actual Sync Server: CORS Proxy GitHub API Allowlist Prefix Bypass Leaks Private Repositories Through the Server GitHub Token

Actual is a local-first personal finance tool. Prior to 26.7.0, Actual Sync Server's CORS proxy is intended to let authenticated users fetch resources only from repositories listed in the official plugin allowlist. When ACTUAL_GITHUB_TOKEN is configured, the proxy automatically attaches the server's GitHub token to GitHub requests. The GitHub API allowlist check uses a raw startsWith() prefix test for /repos/{owner}/{repo} without requiring a path boundary after the repository name. If an allowlisted public plugin repository is https://github.com/acme/plugin, the proxy also accepts GitHub API URLs. Those URLs are outside the allowlisted repository but still pass because their API path starts with /repos/acme/plugin. The proxy then forwards the request with the server's ACTUAL_GITHUB_TOKEN, allowing any authenticated Actual user to read private GitHub resources reachable by that token. Version 26.7.0 fixes the issue.

Record
CVE-2026-57449
CVSS 4.0
7.1 HighCVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N
Weakness
CWE-200, CWE-284, CWE-863
Updated
2026-11-02
OSVGHSA-m62c-5q34-f3cf 7.1

Actual Sync Server: CORS Proxy GitHub API Allowlist Prefix Bypass Leaks Private Repositories Through the Server GitHub Token

Summary

Actual Sync Server's CORS proxy is intended to let authenticated users fetch resources only from repositories listed in the official plugin allowlist. When ACTUAL_GITHUB_TOKEN is configured, the proxy automatically attaches the server's GitHub token to GitHub requests.

The GitHub API allowlist check uses a raw startsWith() prefix test for /repos/{owner}/{repo} without requiring a path boundary after the repository name. If an allowlisted public plugin repository is https://github.com/acme/plugin, the proxy also accepts GitHub API URLs such as:

https://api.github.com/repos/acme/plugin-private/contents/.env
https://api.github.com/repos/acme/plugin-secrets/actions/secrets
https://api.github.com/repos/acme/plugin-internal/releases

Those URLs are outside the allowlisted repository but still pass because their API path starts with /repos/acme/plugin. The proxy then forwards the request with the server's ACTUAL_GITHUB_TOKEN, allowing any authenticated Actual user to read private GitHub resources reachable by that token.

Affected Endpoint

  • GET /cors-proxy?url=...

This endpoint is mounted only when:

if (config.get('corsProxy.enabled')) {
  app.use('/cors-proxy', corsApp.handlers);
}

Source: packages/sync-server/src/app.ts:68-70

Details

The CORS proxy is disabled by default but can be enabled through ACTUAL_CORS_PROXY_ENABLED. The GitHub token is configured through ACTUAL_GITHUB_TOKEN:

github: {
  token: {
    doc: 'GitHub Personal Access Token for API authentication.',
    format: String,
    default: '',
    env: 'ACTUAL_GITHUB_TOKEN',
  },
},
corsProxy: {
  enabled: {
    doc: 'Enable the CORS proxy endpoint.',
    format: Boolean,
    default: false,
    env: 'ACTUAL_CORS_PROXY_ENABLED',
  },
},

Source: packages/sync-server/src/load-config.js:280-296

The proxy fetches the plugin allowlist and stores repository URLs:

const response = await fetch(
  'https://raw.githubusercontent.com/actualbudget/plugin-store/refs/heads/main/plugins.json',
);
...
const plugins = await response.json();
allowlistedRepos = plugins.map(plugin => plugin.url);

Source: packages/sync-server/src/app-cors-proxy.js:43-50

The GitHub API check accepts any path that starts with /repos/${repoOwner}/${repoName}:

for (const repoUrl of allowlistedRepos) {
  const { pathname } = new URL(repoUrl);
  const [, repoOwner, repoName] = pathname.split('/');

  if (
    targetUrl === repoUrl ||
    targetUrl.startsWith(repoUrl + '/') ||
    (hostname === 'api.github.com' &&
      url.pathname.startsWith(`/repos/${repoOwner}/${repoName}`)) ||
    ...
  ) {
    return true;
  }
}

Source: packages/sync-server/src/app-cors-proxy.js:84-99

For api.github.com, there is no delimiter after repoName. This means an allowlisted repo named plugin authorizes API requests for plugin-private, plugin-secrets, plugin-internal, and any other repository under the same owner whose name starts with plugin.

After this incorrect allowlist decision, the proxy attaches the server's GitHub token:

const githubToken = config.get('github.token');
if (
  githubToken &&
  (url.hostname === 'api.github.com' ||
    url.hostname === 'raw.githubusercontent.com' ||
    (url.hostname === 'github.com' && url.pathname.includes('/releases/')))
) {
  requestHeaders['Authorization'] = `Bearer ${githubToken}`;
  requestHeaders['User-Agent'] = 'Actual-Budget-Plugin-System';
}

Source: packages/sync-server/src/app-cors-proxy.js:192-201

Therefore the vulnerable flow is:

  1. A public plugin repository is allowlisted, for example https://github.com/acme/plugin.
  2. The same owner has a private repository with a prefix-matching name, for example acme/plugin-private.
  3. The Actual server has ACTUAL_CORS_PROXY_ENABLED=true.
  4. The Actual server has ACTUAL_GITHUB_TOKEN with access to acme/plugin-private.
  5. Any authenticated Actual user calls:
Record
GHSA-m62c-5q34-f3cf
CVSS 4.0
7.1 HighCVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N
Weakness
CWE-200, CWE-284, CWE-863
Updated
2026-11-02

Affected products

Affected packages

Affected packages
PackageAffectedFixed
@actual-app/sync-server
npm
through 26.6.0
26.7.0
@actual-app/sync-server
npm
before 26.7.0
26.7.0

History

History
RecordedChangedSources
first indexedCVE List, EUVD, GitHub Advisories, NVD, OSV

Dates are when Every CVE recorded each change. Upstream dates may differ.

In short

CVE-2026-57449 is a high vulnerability (CVSS 7.1) in Actualbudget Actual, published September 25, 2026. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.26%, the 17th percentile. Fixed in @actual-app/sync-server 26.7.0.

References

This page doesn't list everything

Left out here: the rest of 2 source descriptions. The JSON record has them.

MoveEnter Open