Moderate severityOSV Advisory· Published Jan 22, 2026· Updated Jan 23, 2026
Gitea: Cross-Repository Authorization Bypass via Release Attachment Linking Leads to Private Attachment Disclosure
CVE-2026-20912
Description
Gitea does not properly validate repository ownership when linking attachments to releases. An attachment uploaded to a private repository could potentially be linked to a release in a different public repository, making it accessible to unauthorized users.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/go-gitea/giteaGo | < 1.25.4 | 1.25.4 |
Affected products
1Patches
1fbea2c68e8dfFix delete attachment check (#36320)
3 files changed · +83 −8
routers/api/v1/repo/release_attachment.go+0 −1 modified@@ -398,7 +398,6 @@ func DeleteReleaseAttachment(ctx *context.APIContext) { ctx.APIErrorNotFound() return } - // FIXME Should prove the existence of the given repo, but results in unnecessary database requests if err := repo_model.DeleteAttachment(ctx, attach, true); err != nil { ctx.APIErrorInternal(err)
routers/web/repo/attachment.go+38 −6 modified@@ -4,11 +4,12 @@ package repo import ( - "fmt" "net/http" + issues_model "code.gitea.io/gitea/models/issues" access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/modules/httpcache" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" @@ -40,7 +41,7 @@ func uploadAttachment(ctx *context.Context, repoID int64, allowedTypes string) { file, header, err := ctx.Req.FormFile("file") if err != nil { - ctx.HTTPError(http.StatusInternalServerError, fmt.Sprintf("FormFile: %v", err)) + ctx.ServerError("FormFile", err) return } defer file.Close() @@ -56,7 +57,7 @@ func uploadAttachment(ctx *context.Context, repoID int64, allowedTypes string) { ctx.HTTPError(http.StatusBadRequest, err.Error()) return } - ctx.HTTPError(http.StatusInternalServerError, fmt.Sprintf("NewAttachment: %v", err)) + ctx.ServerError("UploadAttachmentGeneralSizeLimit", err) return } @@ -74,13 +75,44 @@ func DeleteAttachment(ctx *context.Context) { ctx.HTTPError(http.StatusBadRequest, err.Error()) return } - if !ctx.IsSigned || (ctx.Doer.ID != attach.UploaderID) { + + if !ctx.IsSigned { ctx.HTTPError(http.StatusForbidden) return } + + if attach.RepoID != ctx.Repo.Repository.ID { + ctx.HTTPError(http.StatusBadRequest, "attachment does not belong to this repository") + return + } + + if ctx.Doer.ID != attach.UploaderID { + if attach.IssueID > 0 { + issue, err := issues_model.GetIssueByID(ctx, attach.IssueID) + if err != nil { + ctx.ServerError("GetIssueByID", err) + return + } + if !ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) { + ctx.HTTPError(http.StatusForbidden) + return + } + } else if attach.ReleaseID > 0 { + if !ctx.Repo.Permission.CanWrite(unit.TypeReleases) { + ctx.HTTPError(http.StatusForbidden) + return + } + } else { + if !ctx.Repo.Permission.IsAdmin() && !ctx.Repo.Permission.IsOwner() { + ctx.HTTPError(http.StatusForbidden) + return + } + } + } + err = repo_model.DeleteAttachment(ctx, attach, true) if err != nil { - ctx.HTTPError(http.StatusInternalServerError, fmt.Sprintf("DeleteAttachment: %v", err)) + ctx.ServerError("DeleteAttachment", err) return } ctx.JSON(http.StatusOK, map[string]string{ @@ -114,7 +146,7 @@ func ServeAttachment(ctx *context.Context, uuid string) { } else { // If we have the repository we check access perm, err := access_model.GetUserRepoPermission(ctx, repository, ctx.Doer) if err != nil { - ctx.HTTPError(http.StatusInternalServerError, "GetUserRepoPermission", err.Error()) + ctx.ServerError("GetUserRepoPermission", err) return } if !perm.CanRead(unitType) {
tests/integration/attachment_test.go+45 −1 modified@@ -34,6 +34,14 @@ func testGeneratePngBytes() []byte { } func testCreateIssueAttachment(t *testing.T, session *TestSession, repoURL, filename string, content []byte, expectedStatus int) string { + return testCreateAttachment(t, session, repoURL, "issues", filename, content, expectedStatus) +} + +func testCreateReleaseAttachment(t *testing.T, session *TestSession, repoURL, filename string, content []byte, expectedStatus int) string { + return testCreateAttachment(t, session, repoURL, "releases", filename, content, expectedStatus) +} + +func testCreateAttachment(t *testing.T, session *TestSession, repoURL, issueOrRelease, filename string, content []byte, expectedStatus int) string { body := &bytes.Buffer{} // Setup multi-part @@ -45,7 +53,7 @@ func testCreateIssueAttachment(t *testing.T, session *TestSession, repoURL, file err = writer.Close() assert.NoError(t, err) - req := NewRequestWithBody(t, "POST", repoURL+"/issues/attachments", body) + req := NewRequestWithBody(t, "POST", repoURL+"/"+issueOrRelease+"/attachments", body) req.Header.Add("Content-Type", writer.FormDataContentType()) resp := session.MakeRequest(t, req, expectedStatus) @@ -57,12 +65,23 @@ func testCreateIssueAttachment(t *testing.T, session *TestSession, repoURL, file return obj["uuid"] } +func testDeleteIssueAttachment(t *testing.T, session *TestSession, repoURL, uuid string, expectedStatus int) { + req := NewRequestWithValues(t, "POST", repoURL+"/issues/attachments/remove", map[string]string{"file": uuid}) + session.MakeRequest(t, req, expectedStatus) +} + +func testDeleteReleaseAttachment(t *testing.T, session *TestSession, repoURL, uuid string, expectedStatus int) { + req := NewRequestWithValues(t, "POST", repoURL+"/releases/attachments/remove", map[string]string{"file": uuid}) + session.MakeRequest(t, req, expectedStatus) +} + func TestAttachments(t *testing.T) { defer tests.PrepareTestEnv(t)() t.Run("CreateAnonymousAttachment", testCreateAnonymousAttachment) t.Run("CreateUser2IssueAttachment", testCreateUser2IssueAttachment) t.Run("UploadAttachmentDeleteTemp", testUploadAttachmentDeleteTemp) t.Run("GetAttachment", testGetAttachment) + t.Run("DeleteAttachmentPermissions", testDeleteAttachmentPermissions) } func testUploadAttachmentDeleteTemp(t *testing.T) { @@ -157,3 +176,28 @@ func testGetAttachment(t *testing.T) { }) } } + +func testDeleteAttachmentPermissions(t *testing.T) { + const repoURL = "user2/repo1" + + ownerSession := loginUser(t, "user2") + readonlySession := loginUser(t, "user5") + + issueFromOwner := testCreateIssueAttachment(t, ownerSession, repoURL, "owner-issue.png", testGeneratePngBytes(), http.StatusOK) + testDeleteIssueAttachment(t, readonlySession, repoURL, issueFromOwner, http.StatusForbidden) + + issueFromReader := testCreateIssueAttachment(t, readonlySession, repoURL, "reader-issue.png", testGeneratePngBytes(), http.StatusOK) + testDeleteIssueAttachment(t, ownerSession, repoURL, issueFromReader, http.StatusOK) + + testCreateReleaseAttachment(t, readonlySession, repoURL, "reader-release.png", testGeneratePngBytes(), http.StatusNotFound) + + crossRepoUUID := testCreateIssueAttachment(t, ownerSession, repoURL, "cross-repo.png", testGeneratePngBytes(), http.StatusOK) + testDeleteIssueAttachment(t, ownerSession, "user2/repo2", crossRepoUUID, http.StatusBadRequest) + testDeleteIssueAttachment(t, ownerSession, repoURL, crossRepoUUID, http.StatusOK) + + releaseUUID := testCreateReleaseAttachment(t, ownerSession, repoURL, "reader-release.png", testGeneratePngBytes(), http.StatusOK) + testDeleteReleaseAttachment(t, ownerSession, repoURL, releaseUUID, http.StatusOK) + + // test deleting release attachment from another repo + testDeleteReleaseAttachment(t, ownerSession, "user2/repo2", crossRepoUUID, http.StatusBadRequest) +}
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- github.com/go-gitea/gitea/pull/36320ghsapatchWEB
- github.com/go-gitea/gitea/pull/36355ghsapatchWEB
- github.com/advisories/GHSA-4xx9-vc8v-87hvghsaADVISORY
- github.com/go-gitea/gitea/security/advisories/GHSA-vfmv-f93v-37mwmitrevendor-advisory
- nvd.nist.gov/vuln/detail/CVE-2026-20912ghsaADVISORY
- blog.gitea.com/release-of-1.25.4ghsaWEB
- blog.gitea.com/release-of-1.25.4/mitrerelease-notes
- github.com/go-gitea/gitea/commit/fbea2c68e8df11cfa94e8ead913b79946780ed30ghsaWEB
- github.com/go-gitea/gitea/releases/tag/v1.25.4ghsarelease-notesWEB
News mentions
0No linked articles in our index yet.