fix: add missing checks to several handlers and models (#39839)

Several handlers and model helpers skipped checks that their siblings
already perform.

- Only match principal keys when looking up SSH certificate principals
- Return no attachments for an unsaved issue instead of every issue-less
one
- Reject issue and comment attachments when adding release attachments
- Share one permission check for editing, deleting and reacting to issue
content and for comment attachments between the web UI and the API,
refusing them on locked issues for users who can't write
- Reject issue edits, deletion and label changes in archived
repositories via the API
- Apply cross-origin protection to the repository topics endpoint
- Return 404 for the repositories tab of individual users and respect
profile visibility for the heatmap

Fixes: https://github.com/go-gitea/gitea/issues/39226
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: bircni <bircni@icloud.com>
This commit is contained in:
authored and GitHub committed 2026-10-18 06:29:49 -07:00
1 parent 1031e34987
commit ae17c14cff
28 files changed
+290 -221

No files matched your search

+4
View File
@@ -688,6 +688,8 @@ func EditIssue(ctx *context.APIContext) {
// "$ref": "#/responses/notFound"
// "412":
// "$ref": "#/responses/error"
// "423":
// "$ref": "#/responses/repoArchivedError"
form := web.GetForm[*api.EditIssueOption](ctx)
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
@@ -873,6 +875,8 @@ func DeleteIssue(ctx *context.APIContext) {
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "423":
// "$ref": "#/responses/repoArchivedError"
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
ctx.APIErrorAuto(err)
+5 -5
View File
@@ -10,6 +10,7 @@ import (
"net/http"
issues_model "gitea.dev/models/issues"
"gitea.dev/models/perm"
access_model "gitea.dev/models/perm/access"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unit"
@@ -571,8 +572,7 @@ func editIssueComment(ctx *context.APIContext, form api.EditIssueCommentOption)
ctx.APIErrorAuto(err)
return
}
if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.Permission.CanWriteIssuesOrPulls(comment.Issue.IsPull)) {
if !issue_service.CanSelfChangeOrAccessContent(ctx.Doer, ctx.Repo.Permission, comment.Issue, comment.PosterID, perm.AccessModeWrite) {
ctx.Status(http.StatusForbidden)
return
}
@@ -676,11 +676,11 @@ func deleteIssueComment(ctx *context.APIContext) {
ctx.APIErrorAuto(err)
return
}
if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.Permission.CanWriteIssuesOrPulls(comment.Issue.IsPull)) {
if !issue_service.CanSelfChangeOrAccessContent(ctx.Doer, ctx.Repo.Permission, comment.Issue, comment.PosterID, perm.AccessModeWrite) {
ctx.Status(http.StatusForbidden)
return
} else if !comment.Type.HasContentSupport() {
}
if !comment.Type.HasContentSupport() {
ctx.Status(http.StatusBadRequest)
return
}
@@ -8,6 +8,7 @@ import (
"net/http"
issues_model "gitea.dev/models/issues"
"gitea.dev/models/perm"
repo_model "gitea.dev/models/repo"
user_model "gitea.dev/models/user"
"gitea.dev/modules/log"
@@ -344,21 +345,13 @@ func DeleteIssueCommentAttachment(ctx *context.APIContext) {
}
func getIssueCommentSafe(ctx *context.APIContext) *issues_model.Comment {
comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64("id"))
comment, err := issues_model.GetCommentWithRepoID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("id"))
if err != nil {
ctx.APIErrorAuto(err)
return nil
}
if err := comment.LoadIssue(ctx); err != nil {
ctx.APIErrorInternal(err)
return nil
}
if comment.Issue == nil || comment.Issue.RepoID != ctx.Repo.Repository.ID {
ctx.APIError(http.StatusNotFound, "no matching issue comment found")
return nil
}
if !ctx.Repo.Permission.CanReadIssuesOrPulls(comment.Issue.IsPull) {
ctx.APIErrorNotFound()
return nil
}
@@ -379,7 +372,7 @@ func getIssueCommentAttachmentSafeWrite(ctx *context.APIContext) *repo_model.Att
}
func canUserWriteIssueCommentAttachment(ctx *context.APIContext, comment *issues_model.Comment) bool {
canEditComment := ctx.IsSigned && (ctx.Doer.ID == comment.PosterID || ctx.IsUserRepoAdmin() || ctx.IsUserSiteAdmin()) && ctx.Repo.Permission.CanWriteIssuesOrPulls(comment.Issue.IsPull)
canEditComment := issue_service.CanSelfChangeOrAccessContent(ctx.Doer, ctx.Repo.Permission, comment.Issue, comment.PosterID, perm.AccessModeWrite)
if !canEditComment {
ctx.APIError(http.StatusForbidden, "user should have permission to edit comment")
return false
+8
View File
@@ -102,6 +102,8 @@ func AddIssueLabels(ctx *context.APIContext) {
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "423":
// "$ref": "#/responses/repoArchivedError"
form := web.GetForm[*api.IssueLabelsOption](ctx)
issue, labels, err := prepareForReplaceOrAdd(ctx, *form)
@@ -162,6 +164,8 @@ func DeleteIssueLabel(ctx *context.APIContext) {
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
// "423":
// "$ref": "#/responses/repoArchivedError"
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
@@ -232,6 +236,8 @@ func ReplaceIssueLabels(ctx *context.APIContext) {
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "423":
// "$ref": "#/responses/repoArchivedError"
form := web.GetForm[*api.IssueLabelsOption](ctx)
issue, labels, err := prepareForReplaceOrAdd(ctx, *form)
if err != nil {
@@ -283,6 +289,8 @@ func ClearIssueLabels(ctx *context.APIContext) {
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "423":
// "$ref": "#/responses/repoArchivedError"
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
+4 -19
View File
@@ -8,6 +8,7 @@ import (
"net/http"
issues_model "gitea.dev/models/issues"
"gitea.dev/models/perm"
user_model "gitea.dev/models/user"
api "gitea.dev/modules/structs"
"gitea.dev/modules/web"
@@ -184,28 +185,12 @@ func DeleteIssueCommentReaction(ctx *context.APIContext) {
}
func changeIssueCommentReaction(ctx *context.APIContext, form api.EditReactionOption, isCreateType bool) {
comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64("id"))
comment, err := issues_model.GetCommentWithRepoID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("id"))
if err != nil {
ctx.APIErrorAuto(err)
return
}
if err = comment.LoadIssue(ctx); err != nil {
ctx.APIErrorInternal(err)
return
}
if comment.Issue.RepoID != ctx.Repo.Repository.ID {
ctx.APIErrorNotFound()
return
}
if !ctx.Repo.Permission.CanReadIssuesOrPulls(comment.Issue.IsPull) {
ctx.APIErrorNotFound()
return
}
if comment.Issue.IsLocked && !ctx.Repo.Permission.CanWriteIssuesOrPulls(comment.Issue.IsPull) {
if !issue_service.CanSelfChangeOrAccessContent(ctx.Doer, ctx.Repo.Permission, comment.Issue, comment.PosterID, perm.AccessModeRead) {
ctx.APIError(http.StatusForbidden, "no permission to change reaction")
return
}
@@ -420,7 +405,7 @@ func changeIssueReaction(ctx *context.APIContext, form api.EditReactionOption, i
return
}
if issue.IsLocked && !ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) {
if !issue_service.CanSelfChangeOrAccessContent(ctx.Doer, ctx.Repo.Permission, issue, issue.PosterID, perm.AccessModeRead) {
ctx.APIError(http.StatusForbidden, "no permission to change reaction")
return
}