diff --git a/routers/web/feed/branch.go b/routers/web/feed/branch.go index e7a1cbbe4f..139b7ebc43 100644 --- a/routers/web/feed/branch.go +++ b/routers/web/feed/branch.go @@ -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 { diff --git a/routers/web/feed/file.go b/routers/web/feed/file.go index f115521928..fa590dfcf0 100644 --- a/routers/web/feed/file.go +++ b/routers/web/feed/file.go @@ -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 diff --git a/routers/web/feed/release.go b/routers/web/feed/release.go index 6767c502ff..274eb0cd8a 100644 --- a/routers/web/feed/release.go +++ b/routers/web/feed/release.go @@ -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, diff --git a/routers/web/feed/render.go b/routers/web/feed/render.go index 21ae03a595..d06aa22e64 100644 --- a/routers/web/feed/render.go +++ b/routers/web/feed/render.go @@ -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 == "" { diff --git a/routers/web/feed/repo.go b/routers/web/feed/repo.go index 8c8946eec1..2986e00bb6 100644 --- a/routers/web/feed/repo.go +++ b/routers/web/feed/repo.go @@ -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, diff --git a/tests/integration/feed_repo_test.go b/tests/integration/feed_repo_test.go index 59189bf070..ffe908c74b 100644 --- a/tests/integration/feed_repo_test.go +++ b/tests/integration/feed_repo_test.go @@ -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) + }) + } +}