forgejo-11-secure/tests/integration/git_helper_for_declarative_test.go
Mathieu Fenniak 0b2b072dce [v11.0/forgejo] fix: don't cache write permission on first pushed git reference (#13373)
Forgejo's git pre-receive hook validates whether the authenticated user can write to the target branch during a push operation.  A caching performance optimization in the hook caused the pre-receive hook to validate only the first branch reference in the push.  While typically a push operation has the same rights for all branches on a repository, some exceptions exist which could allow a user to push to a branch, pass the validation, and then bypass checks on additional references due to the cache.  The cache has been removed, forcing all references to be validated for code write permissions.

Thanks to Gitea for identifying this issue in https://github.com/go-gitea/gitea/pull/38103/changes/f25811942cea35233299efdbdfeec40663d4807a. This effort has been re-engineered for Forgejo, consistent with Forgejo's AI Agreement.

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/13373
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
2026-07-09 06:10:00 +02:00

242 lines
8 KiB
Go

// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"context"
"fmt"
"net"
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"regexp"
"strconv"
"testing"
"time"
"forgejo.org/modules/git"
"forgejo.org/modules/setting"
"forgejo.org/modules/util"
"forgejo.org/tests"
gouuid "github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func withKeyFile(t *testing.T, keyname string, callback func(string)) {
tmpDir := t.TempDir()
err := os.Chmod(tmpDir, 0o700)
require.NoError(t, err)
keyFile := filepath.Join(tmpDir, keyname)
pubkey, privkey, err := util.GenerateSSHKeypair()
require.NoError(t, err)
require.NoError(t, os.WriteFile(keyFile, privkey, 0o600))
require.NoError(t, os.WriteFile(keyFile+".pub", pubkey, 0o600))
err = os.WriteFile(path.Join(tmpDir, "ssh"), []byte("#!/bin/bash\n"+
"ssh -o \"UserKnownHostsFile=/dev/null\" -o \"StrictHostKeyChecking=no\" -o \"IdentitiesOnly=yes\" -i \""+keyFile+"\" \"$@\""), 0o700)
require.NoError(t, err)
// Setup ssh wrapper
t.Setenv("GIT_SSH", path.Join(tmpDir, "ssh"))
t.Setenv("GIT_SSH_COMMAND",
"ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o IdentitiesOnly=yes -i \""+keyFile+"\"")
t.Setenv("GIT_SSH_VARIANT", "ssh")
callback(keyFile)
}
func createSSHUrl(gitPath string, u *url.URL) *url.URL {
u2 := *u
u2.Scheme = "ssh"
u2.User = url.User("git")
u2.Host = net.JoinHostPort(setting.SSH.ListenHost, strconv.Itoa(setting.SSH.ListenPort))
u2.Path = gitPath
return &u2
}
var rootPathRe = regexp.MustCompile("\\[repository\\]\nROOT\\s=\\s.*")
func onGiteaRun[T testing.TB](t T, callback func(T, *url.URL)) {
defer tests.PrepareTestEnv(t, 1)()
s := http.Server{
Handler: testWebRoutes,
}
u, err := url.Parse(setting.AppURL)
require.NoError(t, err)
listener, err := net.Listen("tcp", u.Host)
i := 0
for err != nil && i <= 10 {
time.Sleep(100 * time.Millisecond)
listener, err = net.Listen("tcp", u.Host)
i++
}
require.NoError(t, err)
u.Host = listener.Addr().String()
// Override repository root in config.
conf, err := os.ReadFile(setting.CustomConf)
require.NoError(t, err)
require.NoError(t, os.WriteFile(setting.CustomConf, rootPathRe.ReplaceAll(conf, []byte("[repository]\nROOT = "+setting.RepoRootPath)), 0o600))
defer func() {
require.NoError(t, os.WriteFile(setting.CustomConf, conf, 0o600))
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
s.Shutdown(ctx)
cancel()
}()
go s.Serve(listener)
// Started by config go ssh.Listen(setting.SSH.ListenHost, setting.SSH.ListenPort, setting.SSH.ServerCiphers, setting.SSH.ServerKeyExchanges, setting.SSH.ServerMACs)
callback(t, u)
}
func doGitClone(dstLocalPath string, u *url.URL) func(*testing.T) {
return func(t *testing.T) {
t.Helper()
require.NoError(t, git.CloneWithArgs(t.Context(), git.AllowLFSFiltersArgs(), u.String(), dstLocalPath, git.CloneRepoOptions{}))
exist, err := util.IsExist(filepath.Join(dstLocalPath, "README.md"))
require.NoError(t, err)
assert.True(t, exist)
// Set user:password
doGitSetRemoteURL(dstLocalPath, "origin", u)(t)
}
}
func doPartialGitClone(dstLocalPath string, u *url.URL) func(*testing.T) {
return func(t *testing.T) {
t.Helper()
require.NoError(t, git.CloneWithArgs(t.Context(), git.AllowLFSFiltersArgs(), u.String(), dstLocalPath, git.CloneRepoOptions{
Filter: "blob:none",
}))
exist, err := util.IsExist(filepath.Join(dstLocalPath, "README.md"))
require.NoError(t, err)
assert.True(t, exist)
// Set user:password
doGitSetRemoteURL(dstLocalPath, "origin", u)(t)
}
}
func doGitCloneFail(u *url.URL) func(*testing.T) {
return func(t *testing.T) {
t.Helper()
tmpDir := t.TempDir()
require.Error(t, git.Clone(git.DefaultContext, u.String(), tmpDir, git.CloneRepoOptions{}))
exist, err := util.IsExist(filepath.Join(tmpDir, "README.md"))
require.NoError(t, err)
assert.False(t, exist)
}
}
func doGitInitTestRepository(dstPath string, objectFormat git.ObjectFormat) func(*testing.T) {
return func(t *testing.T) {
t.Helper()
// Init repository in dstPath
require.NoError(t, git.InitRepository(git.DefaultContext, dstPath, false, objectFormat.Name()))
// forcibly set default branch to master
_, _, err := git.NewCommand(git.DefaultContext, "symbolic-ref", "HEAD", git.BranchPrefix+"master").RunStdString(&git.RunOpts{Dir: dstPath})
require.NoError(t, err)
require.NoError(t, os.WriteFile(filepath.Join(dstPath, "README.md"), []byte(fmt.Sprintf("# Testing Repository\n\nOriginally created in: %s", dstPath)), 0o644))
require.NoError(t, git.AddChanges(dstPath, true))
signature := git.Signature{
Email: "test@example.com",
Name: "test",
When: time.Now(),
}
require.NoError(t, git.CommitChanges(dstPath, git.CommitChangesOptions{
Committer: &signature,
Author: &signature,
Message: "Initial Commit",
}))
}
}
func doGitAddRemote(dstPath, remoteName string, u *url.URL) func(*testing.T) {
return func(t *testing.T) {
t.Helper()
_, _, err := git.NewCommand(git.DefaultContext, "remote", "add").AddDynamicArguments(remoteName, u.String()).RunStdString(&git.RunOpts{Dir: dstPath})
require.NoError(t, err)
}
}
func doGitSetRemoteURL(dstPath, remoteName string, u *url.URL) func(*testing.T) {
return func(t *testing.T) {
t.Helper()
_, _, err := git.NewCommand(git.DefaultContext, "remote", "set-url").AddDynamicArguments(remoteName, u.String()).RunStdString(&git.RunOpts{Dir: dstPath})
require.NoError(t, err)
}
}
func doGitPushTestRepository(dstPath string, args ...string) func(*testing.T) {
return func(t *testing.T) {
t.Helper()
_, _, err := git.NewCommand(git.DefaultContext, "push", "-u").AddArguments(git.ToTrustedCmdArgs(args)...).RunStdString(&git.RunOpts{Dir: dstPath})
require.NoError(t, err)
}
}
func doGitPushTestRepositoryFail(dstPath string, args ...string) func(*testing.T) {
return func(t *testing.T) {
t.Helper()
_, _, err := git.NewCommand(git.DefaultContext, "push").AddArguments(git.ToTrustedCmdArgs(args)...).RunStdString(&git.RunOpts{Dir: dstPath})
require.Error(t, err)
}
}
func doGitAddSomeCommits(dstPath, branch string) func(*testing.T) {
return func(t *testing.T) {
doGitCheckoutBranch(dstPath, branch)(t)
require.NoError(t, os.WriteFile(filepath.Join(dstPath, fmt.Sprintf("file-%s.txt", branch)), []byte(fmt.Sprintf("file %s %s", branch, gouuid.New().String())), 0o644))
require.NoError(t, git.AddChanges(dstPath, true))
signature := git.Signature{
Email: "test@test.test",
Name: "test",
}
require.NoError(t, git.CommitChanges(dstPath, git.CommitChangesOptions{
Committer: &signature,
Author: &signature,
Message: fmt.Sprintf("update %s", branch),
}))
}
}
func doGitCreateBranch(dstPath, branch string) func(*testing.T) {
return func(t *testing.T) {
t.Helper()
_, _, err := git.NewCommand(git.DefaultContext, "checkout", "-b").AddDynamicArguments(branch).RunStdString(&git.RunOpts{Dir: dstPath})
require.NoError(t, err)
}
}
func doGitCheckoutBranch(dstPath string, args ...string) func(*testing.T) {
return func(t *testing.T) {
t.Helper()
_, _, err := git.NewCommandContextNoGlobals(git.DefaultContext, git.AllowLFSFiltersArgs()...).AddArguments("checkout").AddArguments(git.ToTrustedCmdArgs(args)...).RunStdString(&git.RunOpts{Dir: dstPath})
require.NoError(t, err)
}
}
func doGitPull(dstPath string, args ...string) func(*testing.T) {
return func(t *testing.T) {
t.Helper()
_, _, err := git.NewCommandContextNoGlobals(git.DefaultContext, git.AllowLFSFiltersArgs()...).AddArguments("pull").AddArguments(git.ToTrustedCmdArgs(args)...).RunStdString(&git.RunOpts{Dir: dstPath})
require.NoError(t, err)
}
}
func doGitFetch(dstPath string, args ...string) func(*testing.T) {
return func(t *testing.T) {
t.Helper()
_, _, err := git.NewCommandContextNoGlobals(git.DefaultContext, git.AllowLFSFiltersArgs()...).AddArguments("fetch").AddArguments(git.ToTrustedCmdArgs(args)...).RunStdString(&git.RunOpts{Dir: dstPath})
require.NoError(t, err)
}
}