Jan 24, 2026·7 min read·56 visits
Gitea failed to scope Git LFS lock lookups to the specific repository requesting the deletion. By sending a request to a repository they control, an attacker can supply the Lock ID of a victim's repository. Gitea would verify permissions on the attacker's repo, but fetch and delete the victim's lock from the global database table. This allows for massive workflow disruption and potential binary file corruption.
A critical Insecure Direct Object Reference (IDOR) in Gitea's Git LFS implementation allows authenticated users to delete file locks across any repository on the instance.
Git is amazing at handling text. It merges lines, highlights diffs, and generally makes collaboration possible. But Git is terrible at binary files. Try merging two versions of a 500MB Photoshop file or a compiled game asset. You can't. That's where Git LFS (Large File Storage) comes in.
To prevent two people from editing the same binary file at once (which would result in a 'last save wins' race condition), LFS introduced 'File Locking'. It's the digital equivalent of taking the bathroom key from the gas station counter. Only one person holds the key; everyone else has to wait.
Now, imagine if you walked into that gas station, asked for the key to the bathroom, and the attendant checked your ID, nodded, and then proceeded to remotely unlock the bathroom door at a bank across the street. That is essentially what is happening in CVE-2026-20897.
Gitea, the popular self-hosted Git service, made a classic blunder in database lookup logic. They verified who you were, but they didn't verify where the object you were touching actually lived. The result? A Critical (CVSS 9.1) vulnerability that turns any user with write access into a chaos agent capable of unlocking every file on the server.
The root cause here is a textbook case of Insecure Direct Object Reference (IDOR), specifically via a 'Global Lookup'. When you build a multi-tenant system (or a multi-repository system like Gitea), nearly every database query needs two WHERE clauses: the ID of the object, and the ID of the parent container (the repository).
In the vulnerable version of Gitea, the developers implemented the API endpoint for deleting an LFS lock. The API route looks something like this:
POST /:owner/:repo/info/lfs/locks/delete/:lock_id
The logic flow seemed sound on the surface:
:repo?:lock_id.The fatal flaw was in step 3. The authorization check proved the user owned the repo in the URL. However, the subsequent database query to find the lock ignored the repo entirely. It just asked the database: "Give me the lock with ID 12345." Since database IDs are usually auto-incrementing integers unique across the entire table (not scoped to the repo), lock ID 12345 is globally unique.
If lock 12345 belonged to a private, sensitive repository that the attacker couldn't see, the code didn't care. It fetched the lock object, saw the attacker had permission on the context repository (the one in the URL), and happily deleted the lock belonging to the victim repository.
Let's look at the Go code responsible for this. The vulnerability resided in models/git/lfs_lock.go. In the function GetLFSLockByID, the code was dangerously simple.
// models/git/lfs_lock.go (Vulnerable)
func GetLFSLockByID(ctx context.Context, id int64) (*LFSLock, error) {
lock := new(LFSLock)
// CRITICAL FLAW: Lookup is only by 'id'
has, err := db.GetEngine(ctx).ID(id).Get(lock)
if err != nil {
return nil, err
}
// ... returns lock
}Do you see the missing constraint? It trusts the id implicitly. If I request ID 500, I get the lock object for ID 500, regardless of who owns it. The controller layer then proceeds to delete it.
The patch (Commit da036f3) introduces the necessary scoping. They renamed the function to GetLFSLockByIDAndRepo and forced the repository ID into the query.
// models/git/lfs_lock.go (Patched)
func GetLFSLockByIDAndRepo(ctx context.Context, id, repoID int64) (*LFSLock, error) {
lock := new(LFSLock)
// FIX: Scoped by ID AND repo_id
has, err := db.GetEngine(ctx).ID(id).And("repo_id = ?", repoID).Get(lock)
if err != nil {
return nil, err
}
// ...
}This And("repo_id = ?", repoID) is the difference between a secure application and a critical vulnerability. Now, if an attacker tries to delete lock 12345 via their own repo (Repo ID 99), the database query looks for id=12345 AND repo_id=99. Since lock 12345 actually belongs to Repo ID 1, the query returns nothing, and the exploit fails.
Exploiting this requires an authenticated account and write access to any repository. It does not require access to the victim's repository. Here is how an attacker would weaponize this.
attacker/malice-repo.corp/secret-project uses LFS locks for large assets.First, the attacker needs to guess the Lock ID. Since Gitea uses incremental integers for IDs, this is trivial. If the attacker creates a lock in their own repo and gets ID 5000, they can assume IDs 1 through 4999 exist for other repositories.
The attacker constructs a malicious curl command:
# Target: Lock ID 1337 (belonging to corp/secret-project)
# Context: attacker/malice-repo (where attacker has Admin/Write)
curl -X POST "https://gitea.target.com/api/v1/repos/attacker/malice-repo/git/lfs/locks/1337/unlock" \
-H "Authorization: token <ATTACKER_TOKEN>" \
-H "Content-Type: application/json" \
-d '{"force": true}'The lock is gone. The developer working on secret-project has no idea their file is now unlocked.
Why is this a CVSS 9.1? It's not a data leak (Confidentiality is None). You can't steal the code. But the Integrity and Availability impacts are severe.
In environments that rely heavily on LFS (Game Development, ML Engineering, Video Production), file locking is the only thing preventing data corruption. If an attacker writes a script to loop from ID 1 to 100000 and unlocks everything:
The vulnerability has a scope change (S:C) because the vulnerability in the LFS component affects the integrity of data in completely unrelated security contexts (other repositories).
If you are running Gitea, check your footer. If it says anything less than 1.25.4, you are vulnerable.
app.ini by setting LFS_START_SERVER = false, though this will break LFS functionality for your users.This is a classic lesson in Defense in Depth. Never assume that because a user is authorized to enter a controller method, they are authorized to access the data requested within that method.
Always chain your lookups.
find(id)find(id).where(owner_id: current_user.id)If your ORM or database layer allows global lookups by primary key, you must manually ensure that the object returned belongs to the security context of the request.
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:N/I:H/A:H| Product | Affected Versions | Fixed Version |
|---|---|---|
Gitea Gitea | <= 1.25.3 | 1.25.4 |
| Attribute | Detail |
|---|---|
| CWE | CWE-639 (Authorization Bypass Through User-Controlled Key) |
| CVSS | 9.1 (Critical) |
| Attack Vector | Network (Authenticated) |
| Impact | Integrity / Availability |
| Exploit Status | PoC Available (Theoretical) |
| EPSS Score | 0.00017 |
The application does not verify that the user-provided ID matches the object associated with the current security context.
A high-severity logic inversion flaw in the uniget CLI completely bypasses Sigstore cryptographic signature verification on metadata files by default. If an attacker can poison the package metadata cache or repository, they can execute arbitrary OS commands under the privileges of the active user.
A technical analysis of CVE-2026-59903 in Netty's HTTP CORS handler, where the CorsHandler overwrites existing application Vary headers with Origin, leading to unauthorized caching of sensitive information.
An uncontrolled resource consumption vulnerability in Netty's SctpMessageCompletionHandler allows unauthenticated remote attackers to cause a Denial of Service. By transmitting a series of large, fragmented Stream Control Transmission Protocol (SCTP) messages, an attacker can exhaust the Java Virtual Machine heap or direct memory. This occurs because the handler fails to enforce limits on the cumulative byte size of buffered, incomplete SCTP fragments.
A command injection bypass vulnerability exists in the Glances system monitoring tool prior to v4.5.6. This flaw permits an attacker with local process or container metadata control to bypass action-template sanitizers by reconstructing shell execution operators across adjacent unescaped variables. When a system alert triggers a configured action template, the reconstructed operators are evaluated by the underlying shell, leading to arbitrary code execution in the context of the Glances process.
Grav CMS prior to version 1.7.53 and 2.0.0-rc.8 is vulnerable to an unauthenticated remote denial of service (DoS) vulnerability. By supplying crafted query parameters with extremely large dimensions to image assets, remote unauthenticated attackers can force the server to allocate massive amounts of system memory, leading to kernel Out-Of-Memory (OOM) termination of web worker processes.
CVE-2026-53657 is a local privilege escalation vulnerability in Lima (lima-vm/lima) affecting versions prior to 2.1.3 when configured with the QEMU driver. The guest agent daemon, running as root, creates its communication socket `/run/lima-guestagent.sock` with world-writable permissions (0777). This allows unprivileged local users to command the agent to establish arbitrary tunnels, including to privileged local UNIX sockets (like D-Bus). Because the target daemon authenticates the incoming connection using the credentials of the root-owned guest agent (via SO_PEERCRED), unprivileged users can perform root operations, resulting in complete guest VM compromise.