mirror of
https://github.com/go-gitea/gitea.git
synced 2026-06-14 07:34:11 +00:00
fix: block fork sync when base repo is no longer readable
POST /api/v1/repos/{owner}/{repo}/merge-upstream kept importing commits
from the parent repository even after the parent was switched from public
to private, leaking commits a fork owner could no longer access directly.
Require the doer to still have read access to the base repo's code before
syncing, and map the permission error to 403 (API) / not-found (web).
Assisted-by: Claude:claude-opus-4-8
This commit is contained in:
@@ -1336,6 +1336,9 @@ func MergeUpstream(ctx *context.APIContext) {
|
||||
} else if errors.Is(err, util.ErrNotExist) {
|
||||
ctx.APIError(http.StatusNotFound, err.Error())
|
||||
return
|
||||
} else if errors.Is(err, util.ErrPermissionDenied) {
|
||||
ctx.APIError(http.StatusForbidden, err.Error())
|
||||
return
|
||||
}
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
|
||||
@@ -264,7 +264,7 @@ func MergeUpstream(ctx *context.Context) {
|
||||
branchName := ctx.FormString("branch")
|
||||
_, err := repo_service.MergeUpstream(ctx, ctx.Doer, ctx.Repo.Repository, branchName, false)
|
||||
if err != nil {
|
||||
if errors.Is(err, util.ErrNotExist) {
|
||||
if errors.Is(err, util.ErrNotExist) || errors.Is(err, util.ErrPermissionDenied) {
|
||||
ctx.JSONErrorNotFound()
|
||||
return
|
||||
} else if pull_service.IsErrMergeConflicts(err) {
|
||||
|
||||
@@ -8,7 +8,9 @@ import (
|
||||
"fmt"
|
||||
|
||||
issue_model "gitea.dev/models/issues"
|
||||
access_model "gitea.dev/models/perm/access"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unit"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/git"
|
||||
"gitea.dev/modules/gitrepo"
|
||||
@@ -26,6 +28,17 @@ func MergeUpstream(ctx reqctx.RequestContext, doer *user_model.User, repo *repo_
|
||||
if err = repo.GetBaseRepo(ctx); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// The doer must still be able to read the base repository's code. Otherwise a fork created
|
||||
// while the base repo was public could keep pulling commits after it turned private.
|
||||
basePerm, err := access_model.GetDoerRepoPermission(ctx, repo.BaseRepo, doer)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !basePerm.CanRead(unit.TypeCode) {
|
||||
return "", util.NewPermissionDeniedErrorf("permission denied to read base repo %d", repo.BaseRepo.ID)
|
||||
}
|
||||
|
||||
divergingInfo, err := GetUpstreamDivergingInfo(ctx, repo, branch)
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
||||
@@ -171,5 +171,24 @@ func TestRepoMergeUpstream(t *testing.T) {
|
||||
}).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusBadRequest)
|
||||
})
|
||||
|
||||
t.Run("BasePrivateBlocksSync", func(t *testing.T) {
|
||||
// add a new commit to the base repo, then make the base repo private
|
||||
require.NoError(t, createOrReplaceFileInBranch(baseUser, baseRepo, "secret.txt", "master", "private-content"))
|
||||
baseRepo.IsPrivate = true
|
||||
_, err := db.GetEngine(t.Context()).ID(baseRepo.ID).Cols("is_private").Update(baseRepo)
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
baseRepo.IsPrivate = false
|
||||
_, err := db.GetEngine(t.Context()).ID(baseRepo.ID).Cols("is_private").Update(baseRepo)
|
||||
require.NoError(t, err)
|
||||
}()
|
||||
|
||||
// the fork owner can no longer read the base repo, so syncing must be refused
|
||||
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/test-repo-fork/merge-upstream", forkUser.Name), &api.MergeUpstreamRequest{
|
||||
Branch: "fork-branch",
|
||||
}).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusForbidden)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user