Skip to content

Commit cc80e66

Browse files
algernonearl-warren
authored andcommitted
hooks: Harden when we accept push options that change repo settings
It is possible to change some repo settings (its visibility, and template status) via `git push` options: `-o repo.private=true`, `-o repo.template=true`. Previously, there weren't sufficient permission checks on these, and anyone who could `git push` to a repository - including via an AGit workflow! - was able to change either of these settings. To guard against this, the pre-receive hook will now check if either of these options are present, and if so, will perform additional permission checks to ensure that these can only be set by a repository owner or an administrator. Additionally, changing these settings is disabled for forks, even for the fork's owner. There's still a case where the owner of a repository can change the visibility of it, and it will not propagate to forks (it propagates to forks when changing the visibility via the API), but that's an inconsistency, not a security issue. Signed-off-by: Gergely Nagy <[email protected]>
1 parent b7cff17 commit cc80e66

File tree

2 files changed

+108
-25
lines changed

2 files changed

+108
-25
lines changed

routers/private/hook_pre_receive.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,57 @@ func (ctx *preReceiveContext) AssertCreatePullRequest() bool {
101101
return true
102102
}
103103

104+
func (ctx *preReceiveContext) canChangeSettings() bool {
105+
if !ctx.loadPusherAndPermission() {
106+
return false
107+
}
108+
109+
perm, err := access_model.GetUserRepoPermission(ctx, ctx.Repo.Repository, ctx.user)
110+
if err != nil {
111+
return false
112+
}
113+
if !perm.IsOwner() && !perm.IsAdmin() {
114+
return false
115+
}
116+
117+
if ctx.Repo.Repository.IsFork {
118+
return false
119+
}
120+
121+
return true
122+
}
123+
124+
func (ctx *preReceiveContext) assertChangeSettings() bool {
125+
opts := web.GetForm(ctx).(*private.HookOptions)
126+
127+
if len(opts.GitPushOptions) == 0 {
128+
return true
129+
}
130+
131+
_, hasPrivateOpt := opts.GitPushOptions[private.GitPushOptionRepoPrivate]
132+
_, hasTemplateOpt := opts.GitPushOptions[private.GitPushOptionRepoTemplate]
133+
134+
if !hasPrivateOpt && !hasTemplateOpt {
135+
// If neither `repo.private` nor `repo.template` is present in
136+
// the push options, we're good to go without further permission
137+
// checking.
138+
return true
139+
}
140+
141+
// Either `repo.private` or `repo.template` is among the push options,
142+
// do some permission checks.
143+
if !ctx.canChangeSettings() {
144+
if ctx.Written() {
145+
return false
146+
}
147+
ctx.JSON(http.StatusForbidden, private.Response{
148+
UserMsg: "Permission denied for changing repo settings.",
149+
})
150+
return false
151+
}
152+
return true
153+
}
154+
104155
// HookPreReceive checks whether a individual commit is acceptable
105156
func HookPreReceive(ctx *gitea_context.PrivateContext) {
106157
opts := web.GetForm(ctx).(*private.HookOptions)
@@ -111,6 +162,10 @@ func HookPreReceive(ctx *gitea_context.PrivateContext) {
111162
opts: opts,
112163
}
113164

165+
if !ourCtx.assertChangeSettings() {
166+
return
167+
}
168+
114169
// Iterate across the provided old commit IDs
115170
for i := range opts.OldCommitIDs {
116171
oldCommitID := opts.OldCommitIDs[i]

tests/integration/git_push_test.go

Lines changed: 53 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -8,29 +8,20 @@ import (
88
"testing"
99

1010
"code.gitea.io/gitea/models/db"
11+
repo_model "code.gitea.io/gitea/models/repo"
1112
"code.gitea.io/gitea/models/unittest"
1213
user_model "code.gitea.io/gitea/models/user"
13-
"code.gitea.io/gitea/modules/git"
14+
repo_module "code.gitea.io/gitea/modules/repository"
1415
repo_service "code.gitea.io/gitea/services/repository"
1516

1617
"github.com/stretchr/testify/require"
1718
)
1819

19-
func TestGitPush(t *testing.T) {
20-
onGiteaRun(t, testGitPush)
20+
func TestOptionsGitPush(t *testing.T) {
21+
onGiteaRun(t, testOptionsGitPush)
2122
}
2223

23-
func testGitPush(t *testing.T, u *url.URL) {
24-
t.Run("Push branch with options", func(t *testing.T) {
25-
runTestGitPush(t, u, func(t *testing.T, gitPath string) {
26-
branchName := "branch-with-options"
27-
doGitCreateBranch(gitPath, branchName)(t)
28-
doGitPushTestRepository(gitPath, "origin", branchName, "-o", "repo.private=true", "-o", "repo.template=true")(t)
29-
})
30-
})
31-
}
32-
33-
func runTestGitPush(t *testing.T, u *url.URL, gitOperation func(t *testing.T, gitPath string)) {
24+
func testOptionsGitPush(t *testing.T, u *url.URL) {
3425
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
3526
repo, err := repo_service.CreateRepository(db.DefaultContext, user, user, repo_service.CreateRepoOptions{
3627
Name: "repo-to-push",
@@ -46,22 +37,59 @@ func runTestGitPush(t *testing.T, u *url.URL, gitOperation func(t *testing.T, gi
4637

4738
doGitInitTestRepository(gitPath)(t)
4839

49-
oldPath := u.Path
50-
oldUser := u.User
51-
defer func() {
52-
u.Path = oldPath
53-
u.User = oldUser
54-
}()
5540
u.Path = repo.FullName() + ".git"
5641
u.User = url.UserPassword(user.LowerName, userPassword)
57-
5842
doGitAddRemote(gitPath, "origin", u)(t)
5943

60-
gitRepo, err := git.OpenRepository(git.DefaultContext, gitPath)
61-
require.NoError(t, err)
62-
defer gitRepo.Close()
44+
{
45+
// owner sets private & template to true via push options
46+
branchName := "branch1"
47+
doGitCreateBranch(gitPath, branchName)(t)
48+
doGitPushTestRepository(gitPath, "origin", branchName, "-o", "repo.private=true", "-o", "repo.template=true")(t)
49+
repo, err := repo_model.GetRepositoryByOwnerAndName(db.DefaultContext, user.Name, "repo-to-push")
50+
require.NoError(t, err)
51+
require.True(t, repo.IsPrivate)
52+
require.True(t, repo.IsTemplate)
53+
}
54+
55+
{
56+
// owner sets private & template to false via push options
57+
branchName := "branch2"
58+
doGitCreateBranch(gitPath, branchName)(t)
59+
doGitPushTestRepository(gitPath, "origin", branchName, "-o", "repo.private=false", "-o", "repo.template=false")(t)
60+
repo, err = repo_model.GetRepositoryByOwnerAndName(db.DefaultContext, user.Name, "repo-to-push")
61+
require.NoError(t, err)
62+
require.False(t, repo.IsPrivate)
63+
require.False(t, repo.IsTemplate)
64+
}
65+
66+
{
67+
// create a collaborator with write access
68+
collaborator := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
69+
u.User = url.UserPassword(collaborator.LowerName, userPassword)
70+
doGitAddRemote(gitPath, "collaborator", u)(t)
71+
repo, err := repo_model.GetRepositoryByOwnerAndName(db.DefaultContext, user.Name, "repo-to-push")
72+
require.NoError(t, err)
73+
repo_module.AddCollaborator(db.DefaultContext, repo, collaborator)
74+
}
75+
76+
{
77+
// collaborator with write access is allowed to push
78+
branchName := "branch3"
79+
doGitCreateBranch(gitPath, branchName)(t)
80+
doGitPushTestRepository(gitPath, "collaborator", branchName)(t)
81+
}
6382

64-
gitOperation(t, gitPath)
83+
{
84+
// collaborator with write access fails to change private & template via push options
85+
branchName := "branch4"
86+
doGitCreateBranch(gitPath, branchName)(t)
87+
doGitPushTestRepositoryFail(gitPath, "collaborator", branchName, "-o", "repo.private=true", "-o", "repo.template=true")(t)
88+
repo, err = repo_model.GetRepositoryByOwnerAndName(db.DefaultContext, user.Name, "repo-to-push")
89+
require.NoError(t, err)
90+
require.False(t, repo.IsPrivate)
91+
require.False(t, repo.IsTemplate)
92+
}
6593

6694
require.NoError(t, repo_service.DeleteRepositoryDirectly(db.DefaultContext, user, user.ID, repo.ID))
6795
}

0 commit comments

Comments
 (0)