mirror of
https://github.com/go-gitea/gitea.git
synced 2026-07-25 10:10:31 +02:00
fix(sec): enforce repository token scope on repo feeds
Repository RSS/Atom feed handlers opt into PAT basic auth but never checked the token's scope, so a token without repository scope could read private repository commit history and activity via the feeds, while the sibling download endpoints correctly denied it. Add a token scope check mirroring checkDownloadTokenScope to every repo-content feed function (branch, file, repo activity, releases/tags). Assisted-by: Claude:claude-opus-4-8
This commit is contained in:
@@ -15,6 +15,9 @@ import (
|
||||
|
||||
// ShowBranchFeed shows tags and/or releases on the repo as RSS / Atom feed
|
||||
func ShowBranchFeed(ctx *context.Context, repo *repo.Repository, formatType string) {
|
||||
if !checkRepoFeedTokenScope(ctx) {
|
||||
return
|
||||
}
|
||||
var commits []*git.Commit
|
||||
var err error
|
||||
if ctx.Repo.Commit != nil {
|
||||
|
||||
@@ -16,6 +16,9 @@ import (
|
||||
|
||||
// ShowFileFeed shows tags and/or releases on the repo as RSS / Atom feed
|
||||
func ShowFileFeed(ctx *context.Context, repo *repo.Repository, formatType string) {
|
||||
if !checkRepoFeedTokenScope(ctx) {
|
||||
return
|
||||
}
|
||||
fileName := ctx.Repo.TreePath
|
||||
if len(fileName) == 0 {
|
||||
return
|
||||
|
||||
@@ -15,6 +15,9 @@ import (
|
||||
|
||||
// shows tags and/or releases on the repo as RSS / Atom feed
|
||||
func ShowReleaseFeed(ctx *context.Context, repo *repo_model.Repository, isReleasesOnly bool, formatType string) {
|
||||
if !checkRepoFeedTokenScope(ctx) {
|
||||
return
|
||||
}
|
||||
releases, err := db.Find[repo_model.Release](ctx, repo_model.FindReleasesOptions{
|
||||
IncludeTags: !isReleasesOnly,
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
|
||||
@@ -4,9 +4,18 @@
|
||||
package feed
|
||||
|
||||
import (
|
||||
auth_model "gitea.dev/models/auth"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
|
||||
// checkRepoFeedTokenScope ensures an API token has repository read scope before a
|
||||
// feed serves private repository content, mirroring checkDownloadTokenScope for
|
||||
// downloads. Returns false (and writes the response) when the token is denied.
|
||||
func checkRepoFeedTokenScope(ctx *context.Context) bool {
|
||||
context.CheckRepoScopedToken(ctx, ctx.Repo.Repository, auth_model.Read)
|
||||
return !ctx.Written()
|
||||
}
|
||||
|
||||
// RenderBranchFeed render format for branch or file
|
||||
func RenderBranchFeed(ctx *context.Context, feedType string) {
|
||||
if ctx.Repo.TreePath == "" {
|
||||
|
||||
@@ -16,6 +16,9 @@ import (
|
||||
|
||||
// ShowRepoFeed shows user activity on the repo as RSS / Atom feed
|
||||
func ShowRepoFeed(ctx *context.Context, repo *repo_model.Repository, formatType string) {
|
||||
if !checkRepoFeedTokenScope(ctx) {
|
||||
return
|
||||
}
|
||||
actions, _, err := feed_service.GetFeeds(ctx, activities_model.GetFeedsOptions{
|
||||
RequestedRepo: repo,
|
||||
Actor: ctx.Doer,
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
auth_model "gitea.dev/models/auth"
|
||||
"gitea.dev/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -33,3 +34,41 @@ func TestFeedRepo(t *testing.T) {
|
||||
assert.NotEmpty(t, rss.Channel.Items[0].PubDate)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFeedRepoContentTokenScopes ensures repository feed endpoints enforce the
|
||||
// repository token scope, so a PAT without repository scope cannot read private
|
||||
// repository commit/activity data through RSS/Atom feeds.
|
||||
func TestFeedRepoContentTokenScopes(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
// user2/repo2 is a private repository owned by user2
|
||||
ownerReadToken := getUserToken(t, "user2", auth_model.AccessTokenScopeReadRepository)
|
||||
miscToken := getUserToken(t, "user2", auth_model.AccessTokenScopeReadMisc)
|
||||
|
||||
urls := []string{
|
||||
"/user2/repo2.rss",
|
||||
"/user2/repo2.atom",
|
||||
"/user2/repo2/rss/branch/master",
|
||||
"/user2/repo2/atom/branch/master",
|
||||
"/user2/repo2/rss/branch/master/README.md",
|
||||
"/user2/repo2/tags.rss",
|
||||
"/user2/repo2/tags.atom",
|
||||
"/user2/repo2/releases.rss",
|
||||
"/user2/repo2/releases.atom",
|
||||
}
|
||||
|
||||
for _, url := range urls {
|
||||
t.Run(url, func(t *testing.T) {
|
||||
// feed routes only accept basic auth, so authenticate as the advisory PoC does (user:token)
|
||||
reqDenied := NewRequest(t, "GET", url)
|
||||
reqDenied.SetBasicAuth("user2", miscToken)
|
||||
// a token without repository scope must be denied
|
||||
MakeRequest(t, reqDenied, http.StatusForbidden)
|
||||
|
||||
reqAllowed := NewRequest(t, "GET", url)
|
||||
reqAllowed.SetBasicAuth("user2", ownerReadToken)
|
||||
// a token with repository read scope is allowed
|
||||
MakeRequest(t, reqAllowed, http.StatusOK)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user