diff --git a/modules/git/redirection.go b/modules/git/redirection.go new file mode 100644 index 00000000000..ee7a365482d --- /dev/null +++ b/modules/git/redirection.go @@ -0,0 +1,15 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package git + +import "gitea.dev/modules/git/gitcmd" + +func HandleGitCmdHTTPRedirection(cmd *gitcmd.Command, targets ...string) { + // Protect from SSRF vector (e.g. migrating from an attacker URL). + // cmd.AddConfig("http.followRedirects", "false") + // However, we can't do so at the moment: + // this fails due to 301: git -c http.followRedirects=false clone -v https://gitlab.com/{owner}/{repo} + // this succeeds: git -c http.followRedirects=false clone -v https://gitlab.com/{owner}/{repo}.git + // FIXME: GIT-CLONE-HTTP-REDIRECT-SSRF: need a complete solution in the future +} diff --git a/modules/git/repo.go b/modules/git/repo.go index 6c763c0c801..4683ebe5d85 100644 --- a/modules/git/repo.go +++ b/modules/git/repo.go @@ -151,9 +151,7 @@ func Clone(ctx context.Context, from, to string, opts CloneRepoOptions) error { } cmd := gitcmd.NewCommand().AddArguments("clone") - // Never follow HTTP redirects: no clone caller needs them, and a remote redirecting to an - // otherwise-blocked address would be an SSRF vector (e.g. migrating from an attacker URL). - cmd.AddArguments("-c", "http.followRedirects=false") + HandleGitCmdHTTPRedirection(cmd, from, to) if opts.SkipTLSVerify { cmd.AddArguments("-c", "http.sslVerify=false") } diff --git a/modules/git/repo_test.go b/modules/git/repo_test.go index 5e53d9f0eb9..6d21d9c922c 100644 --- a/modules/git/repo_test.go +++ b/modules/git/repo_test.go @@ -26,6 +26,7 @@ func TestRepoIsEmpty(t *testing.T) { // TestCloneRefusesRedirects ensures Clone never follows HTTP redirects, so a remote // cannot redirect to an otherwise-blocked address (SSRF, e.g. during migration). func TestCloneRefusesRedirects(t *testing.T) { + t.Skip("FIXME: GIT-CLONE-HTTP-REDIRECT-SSRF: need a complete solution in the future") var targetHit atomic.Bool target := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { targetHit.Store(true) diff --git a/services/mirror/mirror_pull.go b/services/mirror/mirror_pull.go index 34a1cabfbc4..b317a8776de 100644 --- a/services/mirror/mirror_pull.go +++ b/services/mirror/mirror_pull.go @@ -71,8 +71,8 @@ func UpdateAddress(ctx context.Context, m *repo_model.Mirror, addr string) error } func pruneBrokenReferences(ctx context.Context, m *repo_model.Mirror, repoLogName string, gitRepo git.RepositoryFacade, timeout time.Duration) error { - // Never follow HTTP redirects, see cmdFetch in runSync. - cmd := gitcmd.NewCommand("remote", "prune").AddConfig("http.followRedirects", "false").AddDynamicArguments(m.GetRemoteName()).WithTimeout(timeout) + cmd := gitcmd.NewCommand("remote", "prune").AddDynamicArguments(m.GetRemoteName()).WithTimeout(timeout) + git.HandleGitCmdHTTPRedirection(cmd, m.GetRemoteName()) stdout, _, pruneErr := cmd.WithRepo(gitRepo).RunStdString(ctx) if pruneErr != nil { // sanitize the output, since it may contain the remote address, which may contain a password @@ -129,9 +129,8 @@ func runSync(ctx context.Context, m *repo_model.Mirror) ([]*repo_module.SyncResu // use fetch but not remote update because git fetch support --tags but remote update doesn't cmdFetch := func() *gitcmd.Command { - // Never follow HTTP redirects: a mirror remote that later starts redirecting to an - // otherwise-blocked address would be an SSRF/exfiltration vector on scheduled syncs. - cmd := gitcmd.NewCommand("fetch", "--tags").AddConfig("http.followRedirects", "false") + cmd := gitcmd.NewCommand("fetch", "--tags") + git.HandleGitCmdHTTPRedirection(cmd, m.GetRemoteName()) if m.EnablePrune { cmd.AddArguments("--prune") } @@ -212,9 +211,9 @@ func runSync(ctx context.Context, m *repo_model.Mirror) ([]*repo_module.SyncResu } cmdRemoteUpdatePrune := func() *gitcmd.Command { - // Never follow HTTP redirects, see cmdFetch above. - return gitcmd.NewCommand("remote", "update", "--prune").AddConfig("http.followRedirects", "false"). - AddDynamicArguments(m.GetRemoteName()).WithTimeout(timeout).WithEnv(envs) + cmd := gitcmd.NewCommand("remote", "update", "--prune").AddDynamicArguments(m.GetRemoteName()).WithTimeout(timeout).WithEnv(envs) + git.HandleGitCmdHTTPRedirection(cmd, m.GetRemoteName()) + return cmd } if repo_service.HasWiki(ctx, m.Repo) {