PoC Archive PoC Archive
High CVE-2026-28699 patched

Gitea OAuth2 Scope Enforcement Bypass via HTTP Basic Auth — CVE-2026-28699

by Alardiians · 2026-07-05

Severity
High
CVE
CVE-2026-28699
Category
web
Affected product
Gitea (code.gitea.io/gitea)
Affected versions
<= 1.26.1 (fixed in 1.26.2)
Disclosed
2026-07-05
Patch status
patched

Metadata

FieldValue
Date Added2026-07-05
Last Updated2026-06
Author / ResearcherAlardiians
CVE / AdvisoryCVE-2026-28699
Categoryweb
SeverityHigh
CVSS ScoreNot specified in source
StatusPoC
Tagsgitea, oauth2, scope-bypass, basic-auth, incorrect-authorization, cwe-863, api
RelatedN/A

Affected Target

FieldValue
Software / SystemGitea (code.gitea.io/gitea)
Versions Affected<= 1.26.1 (fixed in 1.26.2)
Language / PlatformPython PoC (requests) against a Go web application
Authentication RequiredYes (a valid, narrowly-scoped OAuth2 access token)
Network Access RequiredYes

Summary

Gitea lets an OAuth2 application obtain an access token restricted to a subset of a user’s permissions (e.g. read:user only), and enforces that restriction through a tokenRequiresScopes middleware. The middleware relies on an ApiTokenScope value that is correctly attached to the request context when the token is presented as a Bearer header, but is never attached when the same token is presented via HTTP Basic auth (Authorization: Basic base64(<token>:x-oauth-basic)). Because the middleware treats a missing scope value as “nothing to check” and returns early, any OAuth2 token switched from Bearer to Basic silently loses all scope enforcement. The PoC provisions a disposable Gitea instance, mints a read:user-only token via the OAuth2 authorization-code flow, and demonstrates that a write request blocked over Bearer succeeds over Basic.


Vulnerability Details

Root Cause

services/auth/basic.go’s OAuth2 branch discards the token’s scope (_, uid := GetOAuthAccessTokenScopeAndUserID(...)) and never sets ApiTokenScope in the request context, while tokenRequiresScopes() fails open (skips enforcement) whenever that key is absent.

Attack Vector

  1. Obtain (or have a user authorize) an OAuth2 application token scoped only to read:user.
  2. Send the token as a Bearer header against a write endpoint — request is correctly blocked (403).
  3. Re-send the identical token as HTTP Basic auth (Authorization: Basic base64(token:x-oauth-basic)) against the same write endpoint.
  4. The scope check is skipped entirely and the write succeeds (200), allowing profile edits, email additions, and repository creation/modification/deletion beyond the token’s granted scope.

Impact

Any OAuth2 app or attacker holding a narrowly-scoped read-only token can perform arbitrary write actions as the authorizing user (profile changes, email additions enabling account takeover paths, repo creation/deletion), nullifying the OAuth2 consent boundary without escalating beyond the user’s own underlying permissions.


Environment / Lab Setup

Target:   Gitea 1.26.1 (vulnerable) / 1.26.2 (patched), run via fetch_binaries.py
Attacker: Python 3 with `requests` (see requirements.txt)

Proof of Concept

PoC Script

See poc.py and fetch_binaries.py in this folder.

1
2
3
pip install -r requirements.txt
python fetch_binaries.py
python poc.py 1.26.1

fetch_binaries.py downloads vulnerable (1.26.1) and patched (1.26.2) Gitea binaries; poc.py spins up a throwaway SQLite-backed Gitea instance, runs the OAuth2 authorization-code flow to mint a read:user-only token, then issues the same write request via Bearer and Basic auth, printing the pass/fail verdict for each.


Detection & Indicators of Compromise

Signs of compromise:

  • Write operations (profile updates, email additions, repo create/delete) from OAuth2 apps that were only ever granted read scopes.
  • Basic-auth requests to Gitea’s API using an OAuth2 access token instead of a password.
  • Unexpected email addresses added to user accounts shortly after an OAuth2 app authorization.

Remediation

ActionDetail
Primary fixUpgrade to Gitea 1.26.2, which sets ApiTokenScope on the Basic-auth OAuth2 path (GHSA-9r5x-wg6m-x2rc).
Interim mitigationDisable or tightly restrict OAuth2 application grants; monitor for Basic-auth API calls carrying OAuth2 tokens; consider making the scope middleware fail closed.

References


Notes

Mirrored from https://github.com/Alardiians/gitea-CVE-2026-28699 on 2026-07-05.

fetch_binaries.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#!/usr/bin/env python3
"""Download the vulnerable (1.26.1) and patched (1.26.2) Gitea binaries for the
current OS/arch into ./bin/. Cross-platform, stdlib only."""
import os, platform, stat, sys, urllib.request

VERSIONS = ["1.26.1", "1.26.2"]
HERE = os.path.dirname(os.path.abspath(__file__))
BIN  = os.path.join(HERE, "bin")

def arch():
    m = platform.machine().lower()
    if m in ("x86_64", "amd64"): return "amd64"
    if m in ("arm64", "aarch64"): return "arm64"
    return "amd64"

def asset(v):
    a = arch()
    if sys.platform.startswith("win"):
        return f"gitea-{v}-windows-4.0-{a}.exe", f"gitea-{v}.exe"
    if sys.platform == "darwin":
        # Gitea publishes a universal darwin build label per minor; 10.12 amd64 / arm64
        plat = "darwin-10.12" if a == "amd64" else "darwin-10.12"
        return f"gitea-{v}-{plat}-{a}", f"gitea-{v}"
    return f"gitea-{v}-linux-{a}", f"gitea-{v}"

def main():
    os.makedirs(BIN, exist_ok=True)
    for v in VERSIONS:
        remote, local = asset(v)
        url = f"https://github.com/go-gitea/gitea/releases/download/v{v}/{remote}"
        dst = os.path.join(BIN, local)
        print(f"[*] {v}: downloading {url}")
        urllib.request.urlretrieve(url, dst)
        if not sys.platform.startswith("win"):
            os.chmod(dst, os.stat(dst).st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH)
        print(f"[+] {v}: ready at {dst} ({os.path.getsize(dst):,} bytes)")

if __name__ == "__main__":
    main()