From c20df84548d22ebd2c557c0d7960070343f3b82e Mon Sep 17 00:00:00 2001 From: Nicolas Date: Sat, 13 Jun 2026 17:36:35 +0200 Subject: [PATCH] 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 --- routers/api/v1/repo/branch.go | 3 +++ routers/web/repo/branch.go | 2 +- services/repository/merge_upstream.go | 13 +++++++++++++ tests/integration/repo_merge_upstream_test.go | 19 +++++++++++++++++++ 4 files changed, 36 insertions(+), 1 deletion(-) diff --git a/routers/api/v1/repo/branch.go b/routers/api/v1/repo/branch.go index 0806858b4da..1b7b6f32b9d 100644 --- a/routers/api/v1/repo/branch.go +++ b/routers/api/v1/repo/branch.go @@ -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 diff --git a/routers/web/repo/branch.go b/routers/web/repo/branch.go index f5972c8db05..fcd328efafd 100644 --- a/routers/web/repo/branch.go +++ b/routers/web/repo/branch.go @@ -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) { diff --git a/services/repository/merge_upstream.go b/services/repository/merge_upstream.go index 61f27b65421..ad45839ba9c 100644 --- a/services/repository/merge_upstream.go +++ b/services/repository/merge_upstream.go @@ -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 diff --git a/tests/integration/repo_merge_upstream_test.go b/tests/integration/repo_merge_upstream_test.go index 44b3c748c10..a4e34c2713f 100644 --- a/tests/integration/repo_merge_upstream_test.go +++ b/tests/integration/repo_merge_upstream_test.go @@ -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) + }) }) }