# Gitea: Public-only repository tokens can update private PR head branches

Gitea's public-only token check only covers the route repo, so a public-only write token can push base commits into a private PR head branch.

- Payout: Research
- Severity: critical
- Program: Gitea
- Found by (vendor credit): ohxorud-dev
- Found by: ohxorud-dev
- Disclosed: 2026-07-21
- CVE: CVE-2026-58443
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-xxjv-752h-3vp2
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-xxjv-752h-3vp2
- Tags: area:web domain:api domain:web impact:integrity impact:privesc kind:research platform:ghsa sev:critical tech:go technique:source-review vuln:access-control vuln:auth-bypass

**Target:** POST /api/v1/repos/{owner}/{repo}/pulls/{index}/update API endpoint
**Root cause:** checkTokenPublicOnly() validates only ctx.Repo.Repository (the public base route repo); IsUserAllowedToUpdate then authorizes the private head repo via ordinary user RBAC without re-applying the token's public-only restriction.
**Steps:**
1. Create a public base repo and a private fork as head repo; add a branch `private-head-update` with a push-triggered Actions workflow.
2. Mint a token with scopes `public-only` + `write:repository` for a user with write access to the private head.
3. Confirm direct write to private repo fails: `POST /api/v1/repos/{head}/contents/...` returns 404.
4. Create PR on public base with `head` = `user/private-head:private-head-update`.
5. Call `POST /api/v1/repos/{public-base}/pulls/{index}/update?style=merge` with the public-only token; returns 200.
6. Observe public base commit pushed into private head branch and a private-repo ActionRun/Job queued for the push event.
**How found:** Code review of the API route group: noticed checkTokenPublicOnly() is applied at the route/base repo level while the handler later loads and authorizes pull.HeadRepo separately, so the token restriction is never re-checked against the head repo.
**Exploitation:** Attacker holding a public-only write token (e.g. leaked CI token) opens a PR from a private head repo to a public base repo they control, then hits the update endpoint on the public route; Gitea merges base into head server-side, writing into the private repo and triggering its push workflows.
**Escalation:** Server-side push into private repo triggers the private repo's push-triggered Actions workflows, potentially executing attacker-influenced code in private CI context.
**Insight:** When a token scope is enforced by middleware against the route repo, any handler that resolves a second repo (head repo, fork, upstream, submodule) from request data is a candidate for scope bypass — trace every repo the handler touches, not just ctx.Repo.

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

**Vulnerable update endpoint route**

```
m.Post("/update", reqToken(), repo.UpdatePullRequest)
```

**Public-only check applied to route repo only**

```
}, repoAssignment(), checkTokenPublicOnly())
```

