VYPR
Moderate severityOSV Advisory· Published Jan 22, 2026· Updated Jan 23, 2026

Gitea Git LFS Lock Deletion Broken Access Control (Cross-Repo IDOR)

CVE-2026-20897

Description

Gitea does not properly validate repository ownership when deleting Git LFS locks. A user with write access to one repository may be able to delete LFS locks belonging to other repositories.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
github.com/go-gitea/giteaGo
< 1.25.41.25.4

Affected products

1

Patches

1
da036f3f35ca

LFS locks must belong to the intended repo (#36344)

https://github.com/go-gitea/giteaLunny XiaoJan 11, 2026via ghsa
3 files changed · +87 5
  • models/git/lfs_lock.go+4 4 modified
    @@ -101,10 +101,10 @@ func GetLFSLock(ctx context.Context, repo *repo_model.Repository, path string) (
     	return rel, nil
     }
     
    -// GetLFSLockByID returns release by given id.
    -func GetLFSLockByID(ctx context.Context, id int64) (*LFSLock, error) {
    +// GetLFSLockByIDAndRepo returns lfs lock by given id and repository id.
    +func GetLFSLockByIDAndRepo(ctx context.Context, id, repoID int64) (*LFSLock, error) {
     	lock := new(LFSLock)
    -	has, err := db.GetEngine(ctx).ID(id).Get(lock)
    +	has, err := db.GetEngine(ctx).ID(id).And("repo_id = ?", repoID).Get(lock)
     	if err != nil {
     		return nil, err
     	} else if !has {
    @@ -153,7 +153,7 @@ func CountLFSLockByRepoID(ctx context.Context, repoID int64) (int64, error) {
     // DeleteLFSLockByID deletes a lock by given ID.
     func DeleteLFSLockByID(ctx context.Context, id int64, repo *repo_model.Repository, u *user_model.User, force bool) (*LFSLock, error) {
     	return db.WithTx2(ctx, func(ctx context.Context) (*LFSLock, error) {
    -		lock, err := GetLFSLockByID(ctx, id)
    +		lock, err := GetLFSLockByIDAndRepo(ctx, id, repo.ID)
     		if err != nil {
     			return nil, err
     		}
    
  • models/git/lfs_lock_test.go+82 0 added
    @@ -0,0 +1,82 @@
    +// Copyright 2026 The Gitea Authors. All rights reserved.
    +// SPDX-License-Identifier: MIT
    +
    +package git
    +
    +import (
    +	"fmt"
    +	"testing"
    +	"time"
    +
    +	repo_model "code.gitea.io/gitea/models/repo"
    +	"code.gitea.io/gitea/models/unittest"
    +	user_model "code.gitea.io/gitea/models/user"
    +
    +	"github.com/stretchr/testify/assert"
    +	"github.com/stretchr/testify/require"
    +)
    +
    +func createTestLock(t *testing.T, repo *repo_model.Repository, owner *user_model.User) *LFSLock {
    +	t.Helper()
    +
    +	path := fmt.Sprintf("%s-%d-%d", t.Name(), repo.ID, time.Now().UnixNano())
    +	lock, err := CreateLFSLock(t.Context(), repo, &LFSLock{
    +		OwnerID: owner.ID,
    +		Path:    path,
    +	})
    +	require.NoError(t, err)
    +	return lock
    +}
    +
    +func TestGetLFSLockByIDAndRepo(t *testing.T) {
    +	require.NoError(t, unittest.PrepareTestDatabase())
    +
    +	repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
    +	repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
    +	user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
    +	user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
    +
    +	lockRepo1 := createTestLock(t, repo1, user2)
    +	lockRepo3 := createTestLock(t, repo3, user4)
    +
    +	fetched, err := GetLFSLockByIDAndRepo(t.Context(), lockRepo1.ID, repo1.ID)
    +	require.NoError(t, err)
    +	assert.Equal(t, lockRepo1.ID, fetched.ID)
    +	assert.Equal(t, repo1.ID, fetched.RepoID)
    +
    +	_, err = GetLFSLockByIDAndRepo(t.Context(), lockRepo1.ID, repo3.ID)
    +	assert.Error(t, err)
    +	assert.True(t, IsErrLFSLockNotExist(err))
    +
    +	_, err = GetLFSLockByIDAndRepo(t.Context(), lockRepo3.ID, repo1.ID)
    +	assert.Error(t, err)
    +	assert.True(t, IsErrLFSLockNotExist(err))
    +}
    +
    +func TestDeleteLFSLockByIDRequiresRepoMatch(t *testing.T) {
    +	require.NoError(t, unittest.PrepareTestDatabase())
    +
    +	repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
    +	repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
    +	user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
    +	user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
    +
    +	lockRepo1 := createTestLock(t, repo1, user2)
    +	lockRepo3 := createTestLock(t, repo3, user4)
    +
    +	_, err := DeleteLFSLockByID(t.Context(), lockRepo3.ID, repo1, user2, true)
    +	assert.Error(t, err)
    +	assert.True(t, IsErrLFSLockNotExist(err))
    +
    +	existing, err := GetLFSLockByIDAndRepo(t.Context(), lockRepo3.ID, repo3.ID)
    +	require.NoError(t, err)
    +	assert.Equal(t, lockRepo3.ID, existing.ID)
    +
    +	deleted, err := DeleteLFSLockByID(t.Context(), lockRepo3.ID, repo3, user4, true)
    +	require.NoError(t, err)
    +	assert.Equal(t, lockRepo3.ID, deleted.ID)
    +
    +	deleted, err = DeleteLFSLockByID(t.Context(), lockRepo1.ID, repo1, user2, false)
    +	require.NoError(t, err)
    +	assert.Equal(t, lockRepo1.ID, deleted.ID)
    +}
    
  • services/lfs/locks.go+1 1 modified
    @@ -90,7 +90,7 @@ func GetListLockHandler(ctx *context.Context) {
     			})
     			return
     		}
    -		lock, err := git_model.GetLFSLockByID(ctx, v)
    +		lock, err := git_model.GetLFSLockByIDAndRepo(ctx, v, repository.ID)
     		if err != nil && !git_model.IsErrLFSLockNotExist(err) {
     			log.Error("Unable to get lock with ID[%s]: Error: %v", v, err)
     		}
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

9

News mentions

0

No linked articles in our index yet.