fix(migrations): only allow local paths for plain git migration (#40006)

Only the plain git migration can clone from a local server path. Other
services (Gitea, GitLab, Gogs, OneDev, GitBucket, Codebase, CodeCommit,
Bitbucket Server) are accessed through their remote API, so a local path
never works, yet their migrate forms advertised "or a local server
path".

- Remove the local path hint from those templates.
- Add `IsMigrateURLAllowedForService` and use it in the web and API
migrate handlers and in `MigrateRepository`, rejecting local paths for
any service other than plain git.
- Add a unit test.

Raised in review of #39913.

_Authored with Codet (AI assistant) on behalf of @lunny._

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
Lunny Xiaoandwxiaoguang authored and GitHub committed 2026-10-25 21:21:19 +00:00
1 parent ff68b39c93
commit f10f7e7f91
15 files changed
+50 -75

No files matched your search

+3 -3
View File
@@ -98,17 +98,17 @@ func Migrate(ctx *context.APIContext) {
}
}
gitServiceType := convert.ToGitServiceType(form.Service)
remoteAddr, err := git.ParseRemoteAddr(form.CloneAddr, form.AuthUsername, form.AuthPassword)
if err == nil {
err = migrations.IsMigrateURLAllowed(remoteAddr, ctx.Doer)
err = migrations.IsMigrateURLAllowedForService(remoteAddr, gitServiceType, ctx.Doer)
}
if err != nil {
handleRemoteAddrError(ctx, err)
return
}
gitServiceType := convert.ToGitServiceType(form.Service)
if form.Mirror && setting.Mirror.DisableNewPull {
ctx.APIError(http.StatusForbidden, "the site administrator has disabled the creation of new pull mirrors")
return
+1 -1
View File
@@ -153,7 +153,7 @@ func MigratePost(ctx *context.Context) {
remoteAddr, err := git.ParseRemoteAddr(form.CloneAddr, form.AuthUsername, form.AuthPassword)
if err == nil {
err = migrations.IsMigrateURLAllowed(remoteAddr, ctx.Doer)
err = migrations.IsMigrateURLAllowedForService(remoteAddr, form.Service, ctx.Doer)
}
if err != nil {
handleMigrateRemoteAddrError(ctx, err, "clone_addr")
+13 -1
View File
@@ -22,6 +22,7 @@ import (
"gitea.dev/modules/log"
base "gitea.dev/modules/migration"
"gitea.dev/modules/setting"
"gitea.dev/modules/structs"
"gitea.dev/modules/util"
)
@@ -35,6 +36,17 @@ func RegisterDownloaderFactory(factory base.DownloaderFactory) {
factories = append(factories, factory)
}
// IsMigrateURLAllowedForService checks the URL like IsMigrateURLAllowed, and additionally rejects local paths
// unless it is a plain git migration, because other services are accessed by their remote API.
func IsMigrateURLAllowedForService(remoteURL string, serviceType structs.GitServiceType, doer *user_model.User) error {
if serviceType != structs.PlainGitService {
if u, err := url.Parse(remoteURL); err == nil && (u.Scheme == "file" || u.Scheme == "") {
return &git.ErrInvalidCloneAddr{Host: remoteURL, IsProtocolInvalid: true}
}
}
return IsMigrateURLAllowed(remoteURL, doer)
}
// IsMigrateURLAllowed checks if an URL is allowed to be migrated from
func IsMigrateURLAllowed(remoteURL string, doer *user_model.User) error {
// Remote address can be HTTP/HTTPS/Git URL or local path.
@@ -79,7 +91,7 @@ func IsMigrateURLAllowed(remoteURL string, doer *user_model.User) error {
// MigrateRepository migrate repository according MigrateOptions
func MigrateRepository(ctx context.Context, doer *user_model.User, ownerName string, opts base.MigrateOptions, messenger base.Messenger) (*repo_model.Repository, error) {
err := IsMigrateURLAllowed(opts.CloneAddr, doer)
err := IsMigrateURLAllowedForService(opts.CloneAddr, opts.GitServiceType, doer)
if err != nil {
return nil, err
}
+14
View File
@@ -11,8 +11,10 @@ import (
"testing"
user_model "gitea.dev/models/user"
"gitea.dev/modules/git"
"gitea.dev/modules/git/gitcmd"
"gitea.dev/modules/setting"
"gitea.dev/modules/structs"
"gitea.dev/modules/test"
"gitea.dev/modules/util"
@@ -71,3 +73,15 @@ func TestMigrateWhiteBlocklist(t *testing.T) {
setting.ImportLocalPaths = old
}
func TestIsMigrateURLAllowedForServiceLocalPath(t *testing.T) {
defer test.MockVariableValue(&setting.ImportLocalPaths, true)()
admin := &user_model.User{IsAdmin: true}
for _, addr := range []string{"/tmp", "file:///tmp"} {
assert.NoError(t, IsMigrateURLAllowedForService(addr, structs.PlainGitService, admin))
for _, svc := range []structs.GitServiceType{structs.GithubService, structs.GiteaService, structs.GitlabService, structs.BitbucketServerService} {
err := IsMigrateURLAllowedForService(addr, svc, admin)
assert.ErrorAs(t, err, new(*git.ErrInvalidCloneAddr), "%s %d", addr, svc)
}
}
}
+1 -7
View File
@@ -10,13 +10,7 @@
{{template "repo/migrate/common-init" (dict "ServiceType" .service)}}
{{template "repo/migrate/variants" .}}
<div class="inline required field">
<label>{{ctx.Locale.Tr "repo.migrate.clone_address"}}</label>
<input name="clone_addr" autofocus required>
<span class="help">
{{ctx.Locale.Tr "repo.migrate.clone_address_desc"}}{{if .ContextUser.CanImportLocal}} {{ctx.Locale.Tr "repo.migrate.clone_local_path"}}{{end}}
</span>
</div>
{{template "repo/migrate/common-clone-addr"}}
<div class="inline field">
<label>{{ctx.Locale.Tr "username"}}</label>
+1 -7
View File
@@ -9,13 +9,7 @@
<form class="ui form left-right-form form-fetch-action" action="{{.Link}}" method="post">
{{template "repo/migrate/common-init" (dict "ServiceType" .service)}}
<div class="inline required field">
<label>{{ctx.Locale.Tr "repo.migrate.clone_address"}}</label>
<input name="clone_addr" autofocus required>
<span class="help">
{{ctx.Locale.Tr "repo.migrate.clone_address_desc"}}{{if .ContextUser.CanImportLocal}} {{ctx.Locale.Tr "repo.migrate.clone_local_path"}}{{end}}
</span>
</div>
{{template "repo/migrate/common-clone-addr"}}
<div class="inline field">
<label>{{ctx.Locale.Tr "username"}}</label>
+1 -7
View File
@@ -9,13 +9,7 @@
<form class="ui form left-right-form form-fetch-action" action="{{.Link}}" method="post">
{{template "repo/migrate/common-init" (dict "ServiceType" .service)}}
<div class="inline required field">
<label>{{ctx.Locale.Tr "repo.migrate.clone_address"}}</label>
<input name="clone_addr" autofocus required>
<span class="help">
{{ctx.Locale.Tr "repo.migrate.clone_address_desc"}}{{if .ContextUser.CanImportLocal}} {{ctx.Locale.Tr "repo.migrate.clone_local_path"}}{{end}}
</span>
</div>
{{template "repo/migrate/common-clone-addr"}}
<div class="inline required field">
<label>{{ctx.Locale.Tr "repo.migrate.codecommit.aws_access_key_id"}}</label>
@@ -0,0 +1,9 @@
{{$allowImportLocal := $.AllowImportLocal}}
<div class="inline required field">
<label>{{ctx.Locale.Tr "repo.migrate.clone_address"}}</label>
<input name="clone_addr" autofocus required>
<span class="help">
{{ctx.Locale.Tr "repo.migrate.clone_address_desc"}}
{{if $allowImportLocal}}{{ctx.Locale.Tr "repo.migrate.clone_local_path"}}{{end}}
</span>
</div>
+1 -7
View File
@@ -8,14 +8,8 @@
{{template "base/alert" .}}
<form class="ui form left-right-form form-fetch-action" action="{{.Link}}" method="post">
{{template "repo/migrate/common-init" (dict "ServiceType" .service)}}
{{template "repo/migrate/common-clone-addr" (dict "AllowImportLocal" ctx.RootData.SignedUser.CanImportLocal)}}
<div class="inline required field">
<label>{{ctx.Locale.Tr "repo.migrate.clone_address"}}</label>
<input name="clone_addr" autofocus required>
<span class="help">
{{ctx.Locale.Tr "repo.migrate.clone_address_desc"}}{{if .ContextUser.CanImportLocal}} {{ctx.Locale.Tr "repo.migrate.clone_local_path"}}{{end}}
</span>
</div>
<div class="inline field">
<label>{{ctx.Locale.Tr "username"}}</label>
<input name="auth_username">
+1 -7
View File
@@ -9,13 +9,7 @@
<form class="ui form left-right-form form-fetch-action" action="{{.Link}}" method="post">
{{template "repo/migrate/common-init" (dict "ServiceType" .service)}}
<div class="inline required field">
<label>{{ctx.Locale.Tr "repo.migrate.clone_address"}}</label>
<input name="clone_addr" autofocus required>
<span class="help">
{{ctx.Locale.Tr "repo.migrate.clone_address_desc"}}{{if .ContextUser.CanImportLocal}} {{ctx.Locale.Tr "repo.migrate.clone_local_path"}}{{end}}
</span>
</div>
{{template "repo/migrate/common-clone-addr"}}
<div class="inline field">
<label>{{ctx.Locale.Tr "username"}}</label>
+1 -7
View File
@@ -9,13 +9,7 @@
<form class="ui form left-right-form form-fetch-action" action="{{.Link}}" method="post">
{{template "repo/migrate/common-init" (dict "ServiceType" .service)}}
<div class="inline required field">
<label>{{ctx.Locale.Tr "repo.migrate.clone_address"}}</label>
<input name="clone_addr" autofocus required>
<span class="help">
{{ctx.Locale.Tr "repo.migrate.clone_address_desc"}}{{if .ContextUser.CanImportLocal}} {{ctx.Locale.Tr "repo.migrate.clone_local_path"}}{{end}}
</span>
</div>
{{template "repo/migrate/common-clone-addr"}}
<div class="inline field">
<label>{{ctx.Locale.Tr "access_token"}}</label>
+1 -7
View File
@@ -9,13 +9,7 @@
<form class="ui form left-right-form form-fetch-action" action="{{.Link}}" method="post">
{{template "repo/migrate/common-init" (dict "ServiceType" .service)}}
<div class="inline required field">
<label>{{ctx.Locale.Tr "repo.migrate.clone_address"}}</label>
<input name="clone_addr" autofocus required>
<span class="help">
{{ctx.Locale.Tr "repo.migrate.clone_address_desc"}}
</span>
</div>
{{template "repo/migrate/common-clone-addr"}}
<div class="inline field">
<label>{{ctx.Locale.Tr "access_token"}}</label>
+1 -7
View File
@@ -9,13 +9,7 @@
<form class="ui form left-right-form form-fetch-action" action="{{.Link}}" method="post">
{{template "repo/migrate/common-init" (dict "ServiceType" .service)}}
<div class="inline required field">
<label>{{ctx.Locale.Tr "repo.migrate.clone_address"}}</label>
<input name="clone_addr" autofocus required>
<span class="help">
{{ctx.Locale.Tr "repo.migrate.clone_address_desc"}}{{if .ContextUser.CanImportLocal}} {{ctx.Locale.Tr "repo.migrate.clone_local_path"}}{{end}}
</span>
</div>
{{template "repo/migrate/common-clone-addr"}}
<div class="inline field">
<label>{{ctx.Locale.Tr "access_token"}}</label>
+1 -7
View File
@@ -9,13 +9,7 @@
<form class="ui form left-right-form form-fetch-action" action="{{.Link}}" method="post">
{{template "repo/migrate/common-init" (dict "ServiceType" .service)}}
<div class="inline required field">
<label>{{ctx.Locale.Tr "repo.migrate.clone_address"}}</label>
<input name="clone_addr" autofocus required>
<span class="help">
{{ctx.Locale.Tr "repo.migrate.clone_address_desc"}}{{if .ContextUser.CanImportLocal}} {{ctx.Locale.Tr "repo.migrate.clone_local_path"}}{{end}}
</span>
</div>
{{template "repo/migrate/common-clone-addr"}}
<div class="inline field">
<label>{{ctx.Locale.Tr "access_token"}}</label>
+1 -7
View File
@@ -9,13 +9,7 @@
<form class="ui form left-right-form form-fetch-action" action="{{.Link}}" method="post">
{{template "repo/migrate/common-init" (dict "ServiceType" .service)}}
<div class="inline required field">
<label>{{ctx.Locale.Tr "repo.migrate.clone_address"}}</label>
<input name="clone_addr" autofocus required>
<span class="help">
{{ctx.Locale.Tr "repo.migrate.clone_address_desc"}}{{if .ContextUser.CanImportLocal}} {{ctx.Locale.Tr "repo.migrate.clone_local_path"}}{{end}}
</span>
</div>
{{template "repo/migrate/common-clone-addr"}}
<div class="inline field">
<label>{{ctx.Locale.Tr "username"}}</label>