A AegiFlow
MEDIUMCVSS 5.4

CVE-2026-58435

Gitea LFS Deploy-Key Privilege Escalation

Published
2026-07-21
Modified
2026-07-21
Aliases
GHSA-rh79-75qm-gwjr
Sources
github-advisory

Summary

## Vulnerability Header | Field | Value | | ------------------- | ----------------------------------------------------------- | | Vulnerability Title | Gitea LFS Deploy-Key Privilege Escalation | | Severity Rating | High | | Bug Category | Insufficient Authorization | | Location | `services/lfs/server.go:268`, `routers/private/serv.go:275` | | Affected Versions | 1.25.5 | ## Executive Summary Gitea's LFS server (`services/lfs/server.go:268`) uses the `UserID` embedded in an LFS JWT to make cross-repository authorization decisions via `LFSObjectAccessible()`. This would be safe if the JWT `UserID` always matched the actual requesting principal — but for deploy keys, `routers/private/serv.go:275` sets `UserID = repo.OwnerID` instead of any identity representing the deploy key itself. As a result, an attacker who holds a write deploy key for any single repo owned by a victim can obtain a legitimate JWT (via the standard SSH `git-lfs-authenticate` flow) that Gitea will honor as if the victim themselves were making the request. The attacker can then exfiltrate LFS objects from any private repo the victim owns — no admin credentials, no server secrets, no brute force required. If the victim is a site administrator, every LFS object on the entire Gitea instance is reachable. Deploy keys exist precisely to grant narrow, single-repo access to CI/CD systems; this vulnerability defeats that isolation entirely for LFS data. ## Root Cause Analysis ### Technical Description The vulnerability is a **trust-boundary confusion** across two independent subsystems. When a deploy key authenticates over SSH, `serv.go` sets `UserID = repo.OwnerID` because the code has no better representation for a deploy key identity (a `FIXME` comment acknowledges this). That `UserID` is baked verbatim into the LFS JWT by `cmd/serv.go`. The JWT is then consumed by `server.go`, which treats `claims.UserID` as the authenticated principal and loads that user object as `ctx.Doer`. When the batch upload handler encounters an object that exists on disk but isn't yet linked to the target repo, it calls `LFSObjectAccessible(ctx, ctx.Doer, oid)` — a global query across all repos the claimed user can see — to decide whether to silently create the cross-repo link. The JWT's `RepoID` claim is verified (so the request is correctly scoped to one repo at the HTTP level), but the `UserID` driving the cross-repo access decision is the repo *owner*, not the deploy key. The attacker ends up holding a valid, server-signed token that impersonates the victim for any LFS authorization check. ### First Faulty Condition The primary bug — where the JWT `UserID` is set incorrectly — is in `serv.go`: | File | `routers/private/serv.go` | | --------- | ------------------------------------------------------------------------------------------------- | | Line | 275 | | Condition | Deploy key branch sets `results.UserID = repo.OwnerID`; the owner's UID is embedded in the JWT and later used as the authenticated principal for cross-repo privilege decisions in `server.go:268` | ```go // routers/private/serv.go:252–278 if key.Type == asymkey_model.KeyTypeDeploy { ... // FIXME: Deploy keys aren't really the owner of the repo pushing changes // however we don't have good way of representing deploy keys in hook.go // so for now use the owner of the repository results.UserName = results.OwnerName results.UserID = repo.OwnerID // ← OWNER's UID, not the deploy key ... } ``` The secondary bug — where the tainted `UserID` is actually misused — is i

Affected packages

EcosystemPackageAffected versionsFixed versions
Gocode.gitea.io/gitea1.27.0

Remediation: Upgrade to 1.27.0 or later.

References

Includes data from the GitHub Advisory Database, licensed under CC-BY 4.0.

CVE® is a registered trademark of The MITRE Corporation. CVE content reproduced under the CVE Terms of Use; copyright designation © MITRE.