From 4ac42f2b2fc78246ab8ffd0feee9196b15da7bbe Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Mon, 2 Mar 2026 15:36:12 -0800 Subject: [PATCH 1/2] Add check redirect for migrations --- modules/lfs/client.go | 4 +-- modules/lfs/http_client.go | 5 +-- services/migrations/gitea_uploader.go | 2 +- services/migrations/github.go | 4 ++- services/migrations/gogs.go | 1 + services/migrations/http_client.go | 21 ++++++++++++- services/migrations/migrate_test.go | 44 +++++++++++++++++++++++++++ services/migrations/onedev.go | 1 + services/mirror/mirror_pull.go | 2 +- services/mirror/mirror_push.go | 2 +- services/repository/migrate.go | 4 +-- 11 files changed, 79 insertions(+), 11 deletions(-) diff --git a/modules/lfs/client.go b/modules/lfs/client.go index f810e5c7aa..d43f0111bd 100644 --- a/modules/lfs/client.go +++ b/modules/lfs/client.go @@ -24,9 +24,9 @@ type Client interface { } // NewClient creates a LFS client -func NewClient(endpoint *url.URL, httpTransport *http.Transport) Client { +func NewClient(endpoint *url.URL, httpTransport *http.Transport, checkRedirect func(req *http.Request, via []*http.Request) error) Client { if endpoint.Scheme == "file" { return newFilesystemClient(endpoint) } - return newHTTPClient(endpoint, httpTransport) + return newHTTPClient(endpoint, httpTransport, checkRedirect) } diff --git a/modules/lfs/http_client.go b/modules/lfs/http_client.go index 4b51193846..f2d5576fe1 100644 --- a/modules/lfs/http_client.go +++ b/modules/lfs/http_client.go @@ -34,7 +34,7 @@ func (c *HTTPClient) BatchSize() int { return setting.LFSClient.BatchSize } -func newHTTPClient(endpoint *url.URL, httpTransport *http.Transport) *HTTPClient { +func newHTTPClient(endpoint *url.URL, httpTransport *http.Transport, checkRedirect func(req *http.Request, via []*http.Request) error) *HTTPClient { if httpTransport == nil { httpTransport = &http.Transport{ Proxy: proxy.Proxy(), @@ -42,7 +42,8 @@ func newHTTPClient(endpoint *url.URL, httpTransport *http.Transport) *HTTPClient } hc := &http.Client{ - Transport: httpTransport, + Transport: httpTransport, + CheckRedirect: checkRedirect, } basic := &BasicTransferAdapter{hc} diff --git a/services/migrations/gitea_uploader.go b/services/migrations/gitea_uploader.go index af9a0e0eaf..4273aac1a5 100644 --- a/services/migrations/gitea_uploader.go +++ b/services/migrations/gitea_uploader.go @@ -129,7 +129,7 @@ func (g *GiteaLocalUploader) CreateRepo(ctx context.Context, repo *base.Reposito Wiki: opts.Wiki, Releases: opts.Releases, // if didn't get releases, then sync them from tags MirrorInterval: opts.MirrorInterval, - }, NewMigrationHTTPTransport()) + }, NewMigrationHTTPTransport(), CheckMigrateRedirect) g.sameApp = strings.HasPrefix(repo.OriginalURL, setting.AppURL) g.repo = r diff --git a/services/migrations/github.go b/services/migrations/github.go index ce631dcd42..a2d98e2cb1 100644 --- a/services/migrations/github.go +++ b/services/migrations/github.go @@ -100,6 +100,7 @@ func NewGithubDownloaderV3(_ context.Context, baseURL, userName, password, token Base: NewMigrationHTTPTransport(), Source: oauth2.ReuseTokenSource(nil, ts), }, + CheckRedirect: CheckMigrateRedirect, } downloader.addClient(client, baseURL) @@ -111,7 +112,8 @@ func NewGithubDownloaderV3(_ context.Context, baseURL, userName, password, token return proxy.Proxy()(req) } client := &http.Client{ - Transport: transport, + Transport: transport, + CheckRedirect: CheckMigrateRedirect, } downloader.addClient(client, baseURL) } diff --git a/services/migrations/gogs.go b/services/migrations/gogs.go index a4f84dbf72..0ca0ecc7b0 100644 --- a/services/migrations/gogs.go +++ b/services/migrations/gogs.go @@ -113,6 +113,7 @@ func (g *GogsDownloader) client(ctx context.Context) *gogs.Client { } return httpTransport.RoundTrip(req.WithContext(ctx)) }), + CheckRedirect: CheckMigrateRedirect, }) return gogsClient } diff --git a/services/migrations/http_client.go b/services/migrations/http_client.go index 0b997e08f4..1108869798 100644 --- a/services/migrations/http_client.go +++ b/services/migrations/http_client.go @@ -5,8 +5,10 @@ package migrations import ( "crypto/tls" + "net" "net/http" + "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/hostmatcher" "code.gitea.io/gitea/modules/proxy" "code.gitea.io/gitea/modules/setting" @@ -15,10 +17,27 @@ import ( // NewMigrationHTTPClient returns a HTTP client for migration func NewMigrationHTTPClient() *http.Client { return &http.Client{ - Transport: NewMigrationHTTPTransport(), + Transport: NewMigrationHTTPTransport(), + CheckRedirect: CheckMigrateRedirect, } } +func CheckMigrateRedirect(req *http.Request, via []*http.Request) error { + redirectURL := req.URL + if redirectURL == nil { + return &git.ErrInvalidCloneAddr{IsURLError: true, Host: ""} + } + if redirectURL.Scheme != "http" && redirectURL.Scheme != "https" { + return &git.ErrInvalidCloneAddr{Host: redirectURL.Host, IsProtocolInvalid: true, IsPermissionDenied: true, IsURLError: true} + } + hostName := redirectURL.Hostname() + if hostName == "" { + return &git.ErrInvalidCloneAddr{IsURLError: true, Host: redirectURL.String()} + } + addrList, _ := net.LookupIP(hostName) + return checkByAllowBlockList(hostName, addrList) +} + // NewMigrationHTTPTransport returns a HTTP transport for migration func NewMigrationHTTPTransport() *http.Transport { return &http.Transport{ diff --git a/services/migrations/migrate_test.go b/services/migrations/migrate_test.go index 03efa6185b..e48e19ae98 100644 --- a/services/migrations/migrate_test.go +++ b/services/migrations/migrate_test.go @@ -5,12 +5,16 @@ package migrations import ( "net" + "net/http" + "net/url" "path/filepath" "testing" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" "github.com/stretchr/testify/assert" ) @@ -113,3 +117,43 @@ func TestAllowBlockList(t *testing.T) { // reset init("", "", false) } + +func TestCheckMigrateRedirect(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + defer test.MockVariableValue(&setting.Migrations.AllowedDomains, "")() + defer test.MockVariableValue(&setting.Migrations.BlockedDomains, "")() + defer test.MockVariableValue(&setting.Migrations.AllowLocalNetworks, false)() + + assert.NoError(t, Init()) + + err := CheckMigrateRedirect(&http.Request{URL: &url.URL{Scheme: "https", Host: "1.2.3.4"}}, nil) + assert.NoError(t, err) + + err = CheckMigrateRedirect(&http.Request{URL: &url.URL{Scheme: "https", Host: "127.0.0.1"}}, nil) + assert.Error(t, err) + var addrErr *git.ErrInvalidCloneAddr + assert.ErrorAs(t, err, &addrErr) + assert.True(t, addrErr.IsPermissionDenied) + + err = CheckMigrateRedirect(&http.Request{URL: nil}, nil) + assert.Error(t, err) + assert.ErrorAs(t, err, &addrErr) + assert.True(t, addrErr.IsURLError) + + err = CheckMigrateRedirect(&http.Request{URL: &url.URL{Scheme: "file", Host: "example.com"}}, nil) + assert.Error(t, err) + assert.ErrorAs(t, err, &addrErr) + assert.True(t, addrErr.IsProtocolInvalid) + assert.True(t, addrErr.IsPermissionDenied) + + err = CheckMigrateRedirect(&http.Request{URL: &url.URL{Scheme: "https"}}, nil) + assert.Error(t, err) + assert.ErrorAs(t, err, &addrErr) + assert.True(t, addrErr.IsURLError) + + setting.Migrations.AllowLocalNetworks = true + assert.NoError(t, Init()) + err = CheckMigrateRedirect(&http.Request{URL: &url.URL{Scheme: "https", Host: "127.0.0.1"}}, nil) + assert.NoError(t, err) +} diff --git a/services/migrations/onedev.go b/services/migrations/onedev.go index a30e36c8b8..d552e5c27b 100644 --- a/services/migrations/onedev.go +++ b/services/migrations/onedev.go @@ -90,6 +90,7 @@ func NewOneDevDownloader(ctx context.Context, baseURL *url.URL, username, passwo } return httpTransport.RoundTrip(req.WithContext(ctx)) }), + CheckRedirect: CheckMigrateRedirect, }, userMap: make(map[int64]*onedevUser), milestoneMap: make(map[int64]string), diff --git a/services/mirror/mirror_pull.go b/services/mirror/mirror_pull.go index 9ce35f9eab..cfc50df44c 100644 --- a/services/mirror/mirror_pull.go +++ b/services/mirror/mirror_pull.go @@ -173,7 +173,7 @@ func runSync(ctx context.Context, m *repo_model.Mirror) ([]*repo_module.SyncResu if m.LFS && setting.LFS.StartServer { log.Trace("SyncMirrors [repo: %-v]: syncing LFS objects...", m.Repo) endpoint := lfs.DetermineEndpoint(remoteURL.String(), m.LFSEndpoint) - lfsClient := lfs.NewClient(endpoint, migrations.NewMigrationHTTPTransport()) + lfsClient := lfs.NewClient(endpoint, migrations.NewMigrationHTTPTransport(), migrations.CheckMigrateRedirect) if err = repo_module.StoreMissingLfsObjectsInRepository(ctx, m.Repo, gitRepo, lfsClient); err != nil { log.Error("SyncMirrors [repo: %-v]: failed to synchronize LFS objects for repository: %v", m.Repo.FullName(), err) } diff --git a/services/mirror/mirror_push.go b/services/mirror/mirror_push.go index 844e18684b..42e2927b3a 100644 --- a/services/mirror/mirror_push.go +++ b/services/mirror/mirror_push.go @@ -145,7 +145,7 @@ func runPushSync(ctx context.Context, m *repo_model.PushMirror) error { defer gitRepo.Close() endpoint := lfs.DetermineEndpoint(remoteURL.String(), "") - lfsClient := lfs.NewClient(endpoint, migrations.NewMigrationHTTPTransport()) + lfsClient := lfs.NewClient(endpoint, migrations.NewMigrationHTTPTransport(), migrations.CheckMigrateRedirect) if err := pushAllLFSObjects(ctx, gitRepo, lfsClient); err != nil { return util.SanitizeErrorCredentialURLs(err) } diff --git a/services/repository/migrate.go b/services/repository/migrate.go index a51791ed29..7788f1d9a4 100644 --- a/services/repository/migrate.go +++ b/services/repository/migrate.go @@ -72,7 +72,7 @@ func cloneWiki(ctx context.Context, repo *repo_model.Repository, opts migration. // MigrateRepositoryGitData starts migrating git related data after created migrating repository func MigrateRepositoryGitData(ctx context.Context, u *user_model.User, repo *repo_model.Repository, opts migration.MigrateOptions, - httpTransport *http.Transport, + httpTransport *http.Transport, checkRedirect func(req *http.Request, via []*http.Request) error, ) (*repo_model.Repository, error) { if u.IsOrganization() { t, err := organization.OrgFromUser(u).GetOwnerTeam(ctx) @@ -160,7 +160,7 @@ func MigrateRepositoryGitData(ctx context.Context, u *user_model.User, if opts.LFS { endpoint := lfs.DetermineEndpoint(opts.CloneAddr, opts.LFSEndpoint) - lfsClient := lfs.NewClient(endpoint, httpTransport) + lfsClient := lfs.NewClient(endpoint, httpTransport, checkRedirect) if err = repo_module.StoreMissingLfsObjectsInRepository(ctx, repo, gitRepo, lfsClient); err != nil { log.Error("Failed to store missing LFS objects for repository: %v", err) return repo, fmt.Errorf("StoreMissingLfsObjectsInRepository: %w", err) From 1229c90c47b6eeaa84f8bc17c55402451e54994c Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Mon, 2 Mar 2026 15:48:27 -0800 Subject: [PATCH 2/2] Fix lint --- modules/lfs/client_test.go | 4 ++-- tests/integration/actions_schedule_test.go | 2 +- tests/integration/mirror_pull_test.go | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/lfs/client_test.go b/modules/lfs/client_test.go index a1369301e0..ec6936f923 100644 --- a/modules/lfs/client_test.go +++ b/modules/lfs/client_test.go @@ -12,10 +12,10 @@ import ( func TestNewClient(t *testing.T) { u, _ := url.Parse("file:///test") - c := NewClient(u, nil) + c := NewClient(u, nil, nil) assert.IsType(t, &FilesystemClient{}, c) u, _ = url.Parse("https://test.com/lfs") - c = NewClient(u, nil) + c = NewClient(u, nil, nil) assert.IsType(t, &HTTPClient{}, c) } diff --git a/tests/integration/actions_schedule_test.go b/tests/integration/actions_schedule_test.go index 43c44ede55..af709e6bb4 100644 --- a/tests/integration/actions_schedule_test.go +++ b/tests/integration/actions_schedule_test.go @@ -162,7 +162,7 @@ func testScheduleUpdateMirrorSync(t *testing.T) { }, false) assert.NoError(t, err) assert.True(t, mirrorRepo.IsMirror) - mirrorRepo, err = repo_service.MigrateRepositoryGitData(t.Context(), user, mirrorRepo, opts, nil) + mirrorRepo, err = repo_service.MigrateRepositoryGitData(t.Context(), user, mirrorRepo, opts, nil, nil) assert.NoError(t, err) mirrorContext := NewAPITestContext(t, user.Name, mirrorRepo.Name, auth_model.AccessTokenScopeWriteRepository) diff --git a/tests/integration/mirror_pull_test.go b/tests/integration/mirror_pull_test.go index 7902dc10cb..102b6c9c36 100644 --- a/tests/integration/mirror_pull_test.go +++ b/tests/integration/mirror_pull_test.go @@ -51,7 +51,7 @@ func TestMirrorPull(t *testing.T) { assert.NoError(t, err) assert.True(t, mirrorRepo.IsMirror, "expected pull-mirror repo to be marked as a mirror immediately after its creation") - mirrorRepo, err = repo_service.MigrateRepositoryGitData(ctx, user, mirrorRepo, opts, nil) + mirrorRepo, err = repo_service.MigrateRepositoryGitData(ctx, user, mirrorRepo, opts, nil, nil) assert.NoError(t, err) // these units should have been enabled