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

+8 -3
View File
@@ -38,15 +38,20 @@ func Home(ctx *context.Context) {
return
}
home(ctx, false)
orgHome(ctx, false)
}
func Repositories(ctx *context.Context) {
home(ctx, true)
orgHome(ctx, true)
}
func home(ctx *context.Context, viewRepositories bool) {
func orgHome(ctx *context.Context, viewRepositories bool) {
org := ctx.Org.Organization
if org == nil {
// some requests may reach here by "user" route patterns, e.g.: "/{owner}/-/repositories"
ctx.NotFound(nil)
return
}
ctx.Data["PageIsUserProfile"] = true
ctx.Data["Title"] = org.DisplayName()
+19 -38
View File
@@ -14,6 +14,7 @@ import (
"gitea.dev/models/db"
issues_model "gitea.dev/models/issues"
"gitea.dev/models/perm"
access_model "gitea.dev/models/perm/access"
project_model "gitea.dev/models/project"
"gitea.dev/models/renderhelper"
@@ -186,8 +187,8 @@ func GetActionIssue(ctx *context.Context) *issues_model.Issue {
return nil
}
issue.Repo = ctx.Repo.Repository
checkIssueRights(ctx, issue)
if ctx.Written() {
if !ctx.Repo.Permission.CanReadIssuesOrPulls(issue.IsPull) {
ctx.NotFound(nil)
return nil
}
if err = issue.LoadAttributes(ctx); err != nil {
@@ -197,13 +198,6 @@ func GetActionIssue(ctx *context.Context) *issues_model.Issue {
return issue
}
func checkIssueRights(ctx *context.Context, issue *issues_model.Issue) {
if issue.IsPull && !ctx.Repo.Permission.CanRead(unit.TypePullRequests) ||
!issue.IsPull && !ctx.Repo.Permission.CanRead(unit.TypeIssues) {
ctx.NotFound(nil)
}
}
func getActionIssues(ctx *context.Context) issues_model.IssueList {
commaSeparatedIssueIDs := ctx.FormString("issue_ids")
if len(commaSeparatedIssueIDs) == 0 {
@@ -481,39 +475,26 @@ func UpdateIssueAssignee(ctx *context.Context) {
ctx.JSONOK()
}
func canSelfChangeOrAccessContent(ctx *context.Context, refIssue *issues_model.Issue, contentPosterID int64, accessMode perm.AccessMode) bool {
if !issue_service.CanSelfChangeOrAccessContent(ctx.Doer, ctx.Repo.Permission, refIssue, contentPosterID, accessMode) {
ctx.HTTPError(http.StatusForbidden)
return false
}
return true
}
// ChangeIssueReaction create a reaction for issue
func ChangeIssueReaction(ctx *context.Context) {
form := web.GetForm[*forms.ReactionForm](ctx)
issue := GetActionIssue(ctx)
if ctx.Written() {
return
}
if !ctx.IsSigned || (ctx.Doer.ID != issue.PosterID && !ctx.Repo.Permission.CanReadIssuesOrPulls(issue.IsPull)) {
if log.IsTrace() {
if ctx.IsSigned {
issueType := "issues"
if issue.IsPull {
issueType = "pulls"
}
log.Trace("Permission Denied: User %-v not the Poster (ID: %d) and cannot read %s in Repo %-v.\n"+
"User in Repo has Permissions: %-+v",
ctx.Doer,
issue.PosterID,
issueType,
ctx.Repo.Repository,
ctx.Repo.Permission)
} else {
log.Trace("Permission Denied: Not logged in")
}
}
ctx.HTTPError(http.StatusForbidden)
return
}
if ctx.HasError() {
ctx.ServerError("ChangeIssueReaction", errors.New(ctx.GetErrMsg()))
ctx.HTTPError(http.StatusBadRequest, ctx.GetErrMsg())
return
}
issue := GetActionIssue(ctx)
if issue == nil {
return
}
if !canSelfChangeOrAccessContent(ctx, issue, issue.PosterID, perm.AccessModeRead) {
return
}
+44 -109
View File
@@ -12,6 +12,7 @@ import (
git_model "gitea.dev/models/git"
issues_model "gitea.dev/models/issues"
"gitea.dev/models/perm"
"gitea.dev/models/renderhelper"
user_model "gitea.dev/models/user"
"gitea.dev/modules/log"
@@ -146,31 +147,38 @@ func NewComment(ctx *context.Context) {
ctx.JSONRedirect(redirect)
}
func getRepoIssueComment(ctx *context.Context, commentID int64) *issues_model.Comment {
comment, err := issues_model.GetCommentWithRepoID(ctx, ctx.Repo.Repository.ID, commentID)
if err != nil {
ctx.ServerError("GetCommentWithRepoID", err)
return nil
}
if !ctx.Repo.Permission.CanReadIssuesOrPulls(comment.Issue.IsPull) {
ctx.NotFound(nil)
return nil
}
return comment
}
func getRepoIssueCommentForSelfWriteOrAccess(ctx *context.Context, commentID int64, accessMode perm.AccessMode) *issues_model.Comment {
comment := getRepoIssueComment(ctx, commentID)
if comment == nil {
return nil
}
if !canSelfChangeOrAccessContent(ctx, comment.Issue, comment.PosterID, accessMode) {
return nil
}
if !comment.Type.HasContentSupport() {
ctx.HTTPError(http.StatusBadRequest)
return nil
}
return comment
}
// UpdateCommentContent change comment of issue's content
func UpdateCommentContent(ctx *context.Context) {
comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64("id"))
if err != nil {
ctx.ServerError("GetCommentByID", err)
return
}
if err := comment.LoadIssue(ctx); err != nil {
ctx.ServerError("LoadIssue", err)
return
}
if comment.Issue.RepoID != ctx.Repo.Repository.ID {
ctx.NotFound(issues_model.ErrCommentNotExist{})
return
}
if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.Permission.CanWriteIssuesOrPulls(comment.Issue.IsPull)) {
ctx.HTTPError(http.StatusForbidden)
return
}
if !comment.Type.HasContentSupport() {
ctx.HTTPError(http.StatusNoContent)
comment := getRepoIssueCommentForSelfWriteOrAccess(ctx, ctx.PathParamInt64("id"), perm.AccessModeWrite)
if comment == nil {
return
}
@@ -186,7 +194,7 @@ func UpdateCommentContent(ctx *context.Context) {
oldContent := comment.Content
comment.Content = newContent
if err = issue_service.UpdateComment(ctx, comment, contentVersion, ctx.Doer, oldContent); err != nil {
if err := issue_service.UpdateComment(ctx, comment, contentVersion, ctx.Doer, oldContent); err != nil {
if errors.Is(err, user_model.ErrBlockedUser) {
ctx.JSONError(ctx.Tr("repo.issues.comment.blocked_user"))
} else if errors.Is(err, issues_model.ErrCommentAlreadyChanged) {
@@ -216,6 +224,7 @@ func UpdateCommentContent(ctx *context.Context) {
rctx := renderhelper.NewRenderContextRepoComment(ctx, ctx.Repo.Repository, renderhelper.RepoCommentOptions{
FootnoteContextID: strconv.FormatInt(comment.ID, 10),
})
var err error
renderedContent, err = markdown.RenderString(rctx, comment.Content)
if err != nil {
ctx.ServerError("RenderString", err)
@@ -232,31 +241,12 @@ func UpdateCommentContent(ctx *context.Context) {
// DeleteComment delete comment of issue
func DeleteComment(ctx *context.Context) {
comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64("id"))
if err != nil {
ctx.ServerError("GetCommentByID", err)
comment := getRepoIssueCommentForSelfWriteOrAccess(ctx, ctx.PathParamInt64("id"), perm.AccessModeWrite)
if comment == nil {
return
}
if err := comment.LoadIssue(ctx); err != nil {
ctx.ServerError("LoadIssue", err)
return
}
if comment.Issue.RepoID != ctx.Repo.Repository.ID {
ctx.NotFound(issues_model.ErrCommentNotExist{})
return
}
if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.Permission.CanWriteIssuesOrPulls(comment.Issue.IsPull)) {
ctx.HTTPError(http.StatusForbidden)
return
} else if !comment.Type.HasContentSupport() {
ctx.HTTPError(http.StatusNoContent)
return
}
if err = issue_service.DeleteComment(ctx, ctx.Doer, comment); err != nil {
if err := issue_service.DeleteComment(ctx, ctx.Doer, comment); err != nil {
ctx.ServerError("DeleteComment", err)
return
}
@@ -266,51 +256,12 @@ func DeleteComment(ctx *context.Context) {
// ChangeCommentReaction create a reaction for comment
func ChangeCommentReaction(ctx *context.Context) {
comment := getRepoIssueCommentForSelfWriteOrAccess(ctx, ctx.PathParamInt64("id"), perm.AccessModeRead)
if comment == nil {
return
}
form := web.GetForm[*forms.ReactionForm](ctx)
comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64("id"))
if err != nil {
ctx.ServerError("GetCommentByID", err)
return
}
if err := comment.LoadIssue(ctx); err != nil {
ctx.ServerError("LoadIssue", err)
return
}
if comment.Issue.RepoID != ctx.Repo.Repository.ID {
ctx.NotFound(issues_model.ErrCommentNotExist{})
return
}
if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.Permission.CanReadIssuesOrPulls(comment.Issue.IsPull)) {
if log.IsTrace() {
if ctx.IsSigned {
issueType := "issues"
if comment.Issue.IsPull {
issueType = "pulls"
}
log.Trace("Permission Denied: User %-v not the Poster (ID: %d) and cannot read %s in Repo %-v.\n"+
"User in Repo has Permissions: %-+v",
ctx.Doer,
comment.Issue.PosterID,
issueType,
ctx.Repo.Repository,
ctx.Repo.Permission)
} else {
log.Trace("Permission Denied: Not logged in")
}
}
ctx.HTTPError(http.StatusForbidden)
return
}
if !comment.Type.HasContentSupport() {
ctx.HTTPError(http.StatusNoContent)
return
}
switch ctx.PathParam("action") {
case "react":
reaction, err := issue_service.CreateCommentReaction(ctx, ctx.Doer, comment, form.Content)
@@ -338,7 +289,7 @@ func ChangeCommentReaction(ctx *context.Context) {
// Reload new reactions
comment.Reactions = nil
if err = comment.LoadReactions(ctx, ctx.Repo.Repository); err != nil {
if err := comment.LoadReactions(ctx, ctx.Repo.Repository); err != nil {
log.Info("comment.LoadReactions: %s", err)
break
}
@@ -372,24 +323,8 @@ func ChangeCommentReaction(ctx *context.Context) {
// GetCommentAttachments returns attachments for the comment
func GetCommentAttachments(ctx *context.Context) {
comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64("id"))
if err != nil {
ctx.ServerError("GetCommentByID", err)
return
}
if err := comment.LoadIssue(ctx); err != nil {
ctx.ServerError("LoadIssue", err)
return
}
if comment.Issue.RepoID != ctx.Repo.Repository.ID {
ctx.NotFound(issues_model.ErrCommentNotExist{})
return
}
if !ctx.Repo.Permission.CanReadIssuesOrPulls(comment.Issue.IsPull) {
ctx.NotFound(issues_model.ErrCommentNotExist{})
comment := getRepoIssueComment(ctx, ctx.PathParamInt64("id"))
if comment == nil {
return
}
+1 -24
View File
@@ -7,7 +7,6 @@ import (
"net/http"
issues_model "gitea.dev/models/issues"
"gitea.dev/modules/log"
"gitea.dev/modules/templates"
"gitea.dev/services/context"
)
@@ -19,29 +18,7 @@ const (
// IssueWatch sets issue watching
func IssueWatch(ctx *context.Context) {
issue := GetActionIssue(ctx)
if ctx.Written() {
return
}
if !ctx.IsSigned || (ctx.Doer.ID != issue.PosterID && !ctx.Repo.Permission.CanReadIssuesOrPulls(issue.IsPull)) {
if log.IsTrace() {
if ctx.IsSigned {
issueType := "issues"
if issue.IsPull {
issueType = "pulls"
}
log.Trace("Permission Denied: User %-v not the Poster (ID: %d) and cannot read %s in Repo %-v.\n"+
"User in Repo has Permissions: %-+v",
ctx.Doer,
issue.PosterID,
issueType,
ctx.Repo.Repository,
ctx.Repo.Permission)
} else {
log.Trace("Permission Denied: Not logged in")
}
}
ctx.HTTPError(http.StatusForbidden)
if issue == nil {
return
}
+7
View File
@@ -8,6 +8,7 @@ import (
"net/url"
activities_model "gitea.dev/models/activities"
user_model "gitea.dev/models/user"
"gitea.dev/modules/setting"
"gitea.dev/services/context"
)
@@ -51,6 +52,12 @@ func DashboardHeatmap(ctx *context.Context) {
ctx.NotFound(nil)
return
}
if ctx.Org.Organization == nil && !user_model.IsUserVisibleToViewer(ctx, ctx.ContextUser, ctx.Doer) {
// this end point is mounted on different routes, so we need to
// check if the user is visible to the viewer when there is no organization context (no visibility check)
ctx.NotFound(nil)
return
}
var data []*activities_model.UserHeatmapData
var err error
if ctx.Org.Organization == nil {
+1 -1
View File
@@ -1523,7 +1523,7 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
m.Group("/{username}/{reponame}", func() {
m.Post("/topics", repo.TopicsPost)
}, context.RepoAssignment, reqRepoAdmin, context.RepoMustNotBeArchived())
}, optSignIn, context.RepoAssignment, reqRepoAdmin, context.RepoMustNotBeArchived())
m.Group("/{username}/{reponame}", func() {
if setting.Packages.Enabled {