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

Actual Sync Server's CORS proxy prefix-matched GitHub API repo paths, letting any authenticated user read prefix-named private repos with the server's GitHub token.

- Payout: Research
- Severity: high
- Program: Actual Sync Server
- Found by: xIllunight
- Disclosed: 2026-10-07
- CVE: CVE-2026-57449
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-m62c-5q34-f3cf
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-m62c-5q34-f3cf
- Tags: area:web domain:api domain:web impact:data-leak kind:research platform:ghsa sev:high tech:github-actions technique:source-review vuln:access-control vuln:auth-bypass vuln:info-disclosure

**Target:** GET /cors-proxy endpoint with GitHub API allowlist check
**Root cause:** The api.github.com allowlist branch uses a raw startsWith(`/repos/${owner}/${repo}`) test with no path boundary after the repo name, so repos sharing a name prefix pass. The proxy then attaches the server's ACTUAL_GITHUB_TOKEN to the request.
**Steps:**
1. Confirm ACTUAL_CORS_PROXY_ENABLED=true and ACTUAL_GITHUB_TOKEN set with access to a private repo whose name prefixes an allowlisted public plugin repo (e.g. allowlisted `acme/plugin`, private `acme/plugin-private`).
2. Obtain any valid low-privileged Actual session token.
3. Request `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"`.
4. isUrlAllowed() returns true because `/repos/acme/plugin-private/...` startsWith `/repos/acme/plugin`.
5. Proxy forwards with `Authorization: Bearer <server token>` and returns the base64 `content` field.
6. Decode with `echo "UFJPR0RfREJfUEFTU1dPUkQ9Li4u" | base64 -d` to recover secrets.
**How found:** Code review of the allowlist predicate in app-cors-proxy.js: noticed the api.github.com branch used startsWith on `/repos/owner/repo` without a trailing slash boundary, unlike the other branches that append '/'.
**Exploitation:** Any authenticated Actual user hits /cors-proxy with a GitHub API URL for a prefix-matched private repo; the server's PAT is attached and the private repo contents/metadata are returned to the low-privileged user, exposing source, deployment files and committed secrets.
**Escalation:** A single allowlisted public plugin repo becomes a pivot to every private repo under the same owner sharing the name prefix; leaked secrets or release assets can enable supply-chain or infra compromise.
**Insight:** When an allowlist compares URL paths, check every branch for a delimiter after the matched segment; prefix matches on owner/repo names let sibling repos like `plugin-private` inherit authorization, especially dangerous when the server injects its own credential.

## Proof of concept (copied exactly from the original report)

**curl request reading private repo file via proxy**

```
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"
```

