Public mode

No access to your tenant or devices. Every item links to its source. What this means

Some sources, documents or advisories need attention: 274/275 feeds/APIs · 469/469 docs · 8/9 advisories · checked 12:06 UTC Customize Public modeDiscuss ChangeIntel on Discord

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

Collected exploitation and prediction evidence

Risk source coverage

CISA KEV is observed exploitation evidence. FIRST EPSS is a dated prediction of exploitation in the next 30 days. Missing scores are unknown. This collector requests predictions only for explicit CVE aliases in the public NuGet archive; Microsoft release views retain their existing prediction scope.

No collected risk observation for these identifiers. This does not establish absence of exploitation or a zero probability.

cve-program · CVE-2026-57449

Published

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.

Source published
25 Sep 2026 22:10:39 UTC
Source modified
28 Sep 2026 13:07:46 UTC
Observed here
23 Oct 2026 15:48:27 UTC
Source terms
CVE Program Terms of Use

Original source record · Cite this exact observation · Complete original publisher JSON

This view projects the publisher's source fields for reading; the original JSON retains additional fields and CVE enrichment containers. Attribution: cve-program · CVE Program Terms of Use. Source text is retained; field grouping and change labels are provided by ChangeIntel.

CVSS 4.0 · 7.1 CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N

euvd · EUVD-2026-87369

Published

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.

EUVD source date text has no stated timezone and is retained without inventing a UTC instant. Related bulletin membership is not an alias, common cause or applicability claim. A missing vector with a zero score is treated as unrated. Complete vendor/product and underlying source evidence remains in the original JSON.

Original publisher date fields

datePublished: Sep 25, 2026, 10:10:39 PM

dateUpdated: Sep 28, 2026, 1:07:46 PM

Source published
Sep 25, 2026, 10:10:39 PM (timezone not stated)
Source modified
Sep 28, 2026, 1:07:46 PM (timezone not stated)
Observed here
9 Nov 2026 13:15:56 UTC
Source terms
ENISA EUVD · source terms and attribution apply

Original source record · Cite this exact observation · Complete original publisher JSON

This view projects the publisher's source fields for reading; the original JSON retains additional fields and CVE enrichment containers. Attribution: euvd · ENISA EUVD · source terms and attribution apply. Source text is retained; field grouping and change labels are provided by ChangeIntel.

EUVD CVSS 4.0 · 7.1 CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N

nvd · CVE-2026-57449

Published

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.

NVD analysis and CPE criteria are source context, not proof that a package or your environment is affected. Complete affected data, logical configurations and SSVC are retained in the original JSON.

Source analysis status: Deferred

Weakness classifications: CWE-200 · CWE-284 · CWE-863

Original publisher date fields

published: 2026-09-25T23:16:53.990

lastModified: 2026-09-28T14:17:16.140

Source published
25 Sep 2026 23:16:53 UTC
Source modified
28 Sep 2026 14:17:16 UTC
Observed here
25 Oct 2026 14:19:16 UTC
Source terms
NIST NVD usage terms; underlying source attribution retained

Original source record · Cite this exact observation · Complete original publisher JSON

This view projects the publisher's source fields for reading; the original JSON retains additional fields and CVE enrichment containers. Attribution: nvd · NIST NVD usage terms; underlying source attribution retained. Source text is retained; field grouping and change labels are provided by ChangeIntel.

CVSS 4.0 · [email protected] · Secondary · 7.1 CVSS: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

osv · GHSA-m62c-5q34-f3cf

Published

## 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: ```text 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: ```js 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`: ```js 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: ```js 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}`: ```js 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: ```js 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: ```text /cors-proxy?url=https://api.github.com/repos/acme/plugin-private/contents/.env ``` 6. `isUrlAllowed()` returns true because `/repos/acme/plugin-private/...` starts with `/repos/acme/plugin`. 7. The proxy sends the request to GitHub with the server token. 8. The response is returned to the low-privileged Actual user. ## PoC ### Preconditions 1. `ACTUAL_CORS_PROXY_ENABLED=true`. 2. `ACTUAL_GITHUB_TOKEN` is configured and can read a private repo. 3. The official plugin allowlist contains a public repo whose owner and repo name are a prefix of the private target repo. 4. The attacker has any valid Actual session token. Example: - Allowlisted public repo: `https://github.com/acme/plugin` - Private target repo: `https://github.com/acme/plugin-private` - Private file: `.env` ### Manual Reproduction Step 1: Request a private repo file through the Actual CORS proxy: ```bash curl -s "http://TARGET_HOST:5006/cors-proxy?url=https://api.github.com/repos/acme/plugin-private/contents/.env" \ -H "X-Actual-Token: LOW_PRIVILEGED_ACTUAL_SESSION" ``` Vulnerable result: ```json { "name": ".env", "path": ".env", "encoding": "base64", "content": "UFJPRF9EQl9QQVNTV09SRD0uLi4=" } ``` Step 2: Decode the returned `content` field: ```bash echo "UFJPRF9EQl9QQVNTV09SRD0uLi4=" | base64 -d ``` Example decoded output: ```text PROD_DB_PASSWORD=... ``` ### Python PoC Attached separately as `poc_actual_cors_proxy_github_prefix_bypass.py`. The PoC does not need the GitHub token. It uses the target Actual server as the oracle: if the server token can access the prefix-matched private repository, GitHub's API response is returned to the low-privileged Actual user. ## Impact - Any authenticated Actual user can use the server's GitHub token against prefix-matched repositories outside the plugin allowlist. - Private source code, repository metadata, release data, deployment files, and accidentally committed secrets can be exposed. - If the token has broad organization or user repository read permissions, a public allowlisted plugin repo can become a stepping stone to multiple private repos sharing the same name prefix. - Exposed repository secrets or deployment material may enable follow-on compromise of production infrastructure or supply-chain release assets. ## Recommended Remediation - Parse and compare GitHub API repository path segments exactly. - Replace: ```js url.pathname.startsWith(`/repos/${repoOwner}/${repoName}`) ``` with an exact boundary-aware check: ```js url.pathname === `/repos/${repoOwner}/${repoName}` || url.pathname.startsWith(`/repos/${repoOwner}/${repoName}/`) ``` - Apply the same boundary discipline to every allowlist branch. - Add regression tests showing that an allowlisted `owner/plugin` does not authorize `owner/plugin-private`. - Consider never attaching `ACTUAL_GITHUB_TOKEN` for user-driven proxy requests unless the requested repo exactly matches an allowlisted repository.

Source published
7 Oct 2026 14:00:45 UTC
Source modified
7 Oct 2026 14:16:44 UTC
Observed here
24 Oct 2026 13:17:17 UTC
Source terms
GitHub Advisory Database · CC-BY-4.0

Original source record · Cite this exact observation · Complete original publisher JSON

This view projects the publisher's source fields for reading; the original JSON retains additional fields and CVE enrichment containers. Attribution: osv · GitHub Advisory Database · CC-BY-4.0. Source text is retained; field grouping and change labels are provided by ChangeIntel.

CVSS_V4 · CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N

Publisher rating · HIGH

ChangeIntel

An IT change radar: releases, security, known issues, retirements, documentation changes, and service status from public sources. Every item links to supporting evidence; dates and statuses can change after they are read.

Sources read 11 Nov 12:06 UTC · 274 of 275 readable · 273 fully read · documentation 469/469 current