mirror of
https://github.com/go-gitea/gitea.git
synced 2026-08-04 02:12:34 +02:00
fix(sec): block redirects in repository migration clone (SSRF)
Migration validates the initial clone address against the allow/block list, but a git-service migration clones with `git clone`, which follows an HTTP 302 from the remote to an internal address without re-validating. This let a low-privilege user reach internal services through Gitea. Refuse redirects on the migration clone via `http.followRedirects=false`, the only reliable guard since git resolves redirects below Gitea's validation layer. Applied to both the repository and wiki clones. Assisted-by: Claude:claude-opus-4-8
This commit is contained in:
@@ -111,6 +111,9 @@ type CloneRepoOptions struct {
|
||||
SkipTLSVerify bool
|
||||
SingleBranch bool
|
||||
Env []string
|
||||
// NoFollowRedirects refuses HTTP redirects during the clone. It is used for
|
||||
// migrations to stop a remote redirecting to an otherwise-blocked address (SSRF).
|
||||
NoFollowRedirects bool
|
||||
}
|
||||
|
||||
// Clone clones original repository to target path.
|
||||
@@ -124,6 +127,9 @@ func Clone(ctx context.Context, from, to string, opts CloneRepoOptions) error {
|
||||
if opts.SkipTLSVerify {
|
||||
cmd.AddArguments("-c", "http.sslVerify=false")
|
||||
}
|
||||
if opts.NoFollowRedirects {
|
||||
cmd.AddArguments("-c", "http.followRedirects=false")
|
||||
}
|
||||
if opts.Mirror {
|
||||
cmd.AddArguments("--mirror")
|
||||
}
|
||||
|
||||
@@ -4,7 +4,10 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"path/filepath"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -19,3 +22,34 @@ func TestRepoIsEmpty(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, isEmpty)
|
||||
}
|
||||
|
||||
// TestCloneNoFollowRedirects ensures the migration clone refuses HTTP redirects,
|
||||
// so a remote cannot redirect to an otherwise-blocked address (SSRF). Without the
|
||||
// option git follows the redirect and reaches the target.
|
||||
func TestCloneNoFollowRedirects(t *testing.T) {
|
||||
var targetHit atomic.Bool
|
||||
target := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
targetHit.Store(true)
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}))
|
||||
defer target.Close()
|
||||
|
||||
redirect := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, target.URL+r.URL.Path, http.StatusFound)
|
||||
}))
|
||||
defer redirect.Close()
|
||||
|
||||
t.Run("FollowsRedirectByDefault", func(t *testing.T) {
|
||||
targetHit.Store(false)
|
||||
err := Clone(t.Context(), redirect.URL, filepath.Join(t.TempDir(), "dst"), CloneRepoOptions{})
|
||||
assert.Error(t, err)
|
||||
assert.True(t, targetHit.Load(), "git should reach the redirect target without the protection")
|
||||
})
|
||||
|
||||
t.Run("RefusesRedirect", func(t *testing.T) {
|
||||
targetHit.Store(false)
|
||||
err := Clone(t.Context(), redirect.URL, filepath.Join(t.TempDir(), "dst"), CloneRepoOptions{NoFollowRedirects: true})
|
||||
assert.Error(t, err)
|
||||
assert.False(t, targetHit.Load(), "git must not follow the redirect to the target")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -45,10 +45,11 @@ func cloneWiki(ctx context.Context, repo *repo_model.Repository, opts migration.
|
||||
}
|
||||
}
|
||||
if err := gitrepo.CloneExternalRepo(ctx, wikiRemoteURL, storageRepo, git.CloneRepoOptions{
|
||||
Mirror: true,
|
||||
Quiet: true,
|
||||
Timeout: migrateTimeout,
|
||||
SkipTLSVerify: setting.Migrations.SkipTLSVerify,
|
||||
Mirror: true,
|
||||
Quiet: true,
|
||||
Timeout: migrateTimeout,
|
||||
SkipTLSVerify: setting.Migrations.SkipTLSVerify,
|
||||
NoFollowRedirects: true,
|
||||
}); err != nil {
|
||||
log.Error("Clone wiki failed, err: %v", err)
|
||||
cleanIncompleteWikiPath()
|
||||
@@ -91,10 +92,11 @@ func MigrateRepositoryGitData(ctx context.Context, u *user_model.User,
|
||||
}
|
||||
|
||||
if err := gitrepo.CloneExternalRepo(ctx, opts.CloneAddr, repo, git.CloneRepoOptions{
|
||||
Mirror: true,
|
||||
Quiet: true,
|
||||
Timeout: migrateTimeout,
|
||||
SkipTLSVerify: setting.Migrations.SkipTLSVerify,
|
||||
Mirror: true,
|
||||
Quiet: true,
|
||||
Timeout: migrateTimeout,
|
||||
SkipTLSVerify: setting.Migrations.SkipTLSVerify,
|
||||
NoFollowRedirects: true,
|
||||
}); err != nil {
|
||||
if errors.Is(err, context.DeadlineExceeded) {
|
||||
return repo, fmt.Errorf("clone timed out, consider increasing [git.timeout] MIGRATE in app.ini, underlying err: %w", err)
|
||||
|
||||
Reference in New Issue
Block a user