# OneUptime has broken access control in GitHub App installation flow that allows unauthorized project binding

OneUptime's GitHub App callback trusts unsigned base64 state and installation_id, letting an attacker overwrite any project's GitHub App installation binding.

- Payout: Research
- Severity: high
- Program: OneUptime
- Found by: maru1009
- Disclosed: 2026-03-09
- CVE: CVE-2026-30920
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-656w-6f6c-m9r6
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-656w-6f6c-m9r6
- Tags: area:web domain:web impact:integrity kind:research platform:ghsa sev:high tech:oauth technique:source-review vuln:access-control vuln:auth-bypass vuln:idor

**Target:** GitHub App OAuth callback endpoint (/api/github/auth/callback) and related repository endpoints
**Root cause:** The callback decodes attacker-controlled base64 JSON from `state` and uses the embedded projectId directly, writing installation_id with isRoot:true; userId is only checked for presence, not authenticity, and state is not signed or session-bound.
**Steps:**
1. Craft state as base64 of `{"projectId":"<victim-project-uuid>","userId":"x"}`
2. Request `GET /api/github/auth/callback?installation_id=999999999&state=${STATE}`
3. Server decodes state, uses projectId, and calls ProjectService.updateOneById with isRoot:true setting gitHubAppInstallationId
4. Observe 302 redirect to `/dashboard/<victim-project-uuid>/code-repository?installation_id=999999999` and overwritten binding
5. Optionally use a valid installation ID against repository listing/connect endpoints to enumerate repos and create CodeRepository records in arbitrary projects
**How found:** Code review of the GitHub App install/callback flow: noticed state is plain base64 JSON rather than a signed/session-bound token, projectId is used directly, and userId is only presence-checked; then traced follow-on endpoints and middleware allowing no-token Public requests.
**Exploitation:** Unauthenticated attacker sends a crafted callback URL with base64 state naming a victim projectId and an arbitrary installation_id; server overwrites the project's GitHub App installation binding with root privileges. Follow-on endpoints mint installation tokens from any valid installation ID, enabling repo enumeration and CodeRepository creation in arbitrary projects.
**Escalation:** Chaining the callback overwrite with repository listing/connect endpoints (which accept no-token Public requests) to disclose repo metadata and create CodeRepository records cross-project.
**Insight:** In OAuth/App install flows, check whether `state` is signed or session-bound; if it's just base64 JSON, the embedded tenant/project ID becomes an attacker-controlled key for cross-tenant writes, especially when the write uses root/system privileges.

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

**Curl PoC overwriting victim project's GitHub installation**

```
STATE=$(printf '%s' '{"projectId":"<victim-project-uuid>","userId":"x"}' | base64 | tr -d '\n')
curl -isk "https://<host>/api/github/auth/callback?installation_id=999999999&state=${STATE}"
```

