summaryrefslogtreecommitdiffstats
path: root/services/migrations
diff options
context:
space:
mode:
authorwxiaoguang <wxiaoguang@gmail.com>2021-11-20 17:34:05 +0800
committerGitHub <noreply@github.com>2021-11-20 17:34:05 +0800
commit013fb73068281b45b33c72abaae0c42c8d79c499 (patch)
tree5cb710ea15a6f471648ecf19e2fdfab9804cb084 /services/migrations
parentc96be0cd982255f20a3fe6ff4683115b8073e65e (diff)
downloadgitea-013fb73068281b45b33c72abaae0c42c8d79c499.tar.gz
gitea-013fb73068281b45b33c72abaae0c42c8d79c499.zip
Use `hostmatcher` to replace `matchlist`, improve security (#17605)
Use hostmacher to replace matchlist. And we introduce a better DialContext to do a full host/IP check, otherwise the attackers can still bypass the allow/block list by a 302 redirection.
Diffstat (limited to 'services/migrations')
-rw-r--r--services/migrations/gitea_downloader.go17
-rw-r--r--services/migrations/gitea_uploader.go2
-rw-r--r--services/migrations/github.go29
-rw-r--r--services/migrations/gitlab.go19
-rw-r--r--services/migrations/gogs.go13
-rw-r--r--services/migrations/http_client.go30
-rw-r--r--services/migrations/migrate.go71
-rw-r--r--services/migrations/migrate_test.go8
8 files changed, 93 insertions, 96 deletions
diff --git a/services/migrations/gitea_downloader.go b/services/migrations/gitea_downloader.go
index 00180adf41..258f030726 100644
--- a/services/migrations/gitea_downloader.go
+++ b/services/migrations/gitea_downloader.go
@@ -6,7 +6,6 @@ package migrations
import (
"context"
- "crypto/tls"
"errors"
"fmt"
"io"
@@ -18,8 +17,6 @@ import (
admin_model "code.gitea.io/gitea/models/admin"
"code.gitea.io/gitea/modules/log"
base "code.gitea.io/gitea/modules/migration"
- "code.gitea.io/gitea/modules/proxy"
- "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
gitea_sdk "code.gitea.io/sdk/gitea"
@@ -90,12 +87,7 @@ func NewGiteaDownloader(ctx context.Context, baseURL, repoPath, username, passwo
gitea_sdk.SetToken(token),
gitea_sdk.SetBasicAuth(username, password),
gitea_sdk.SetContext(ctx),
- gitea_sdk.SetHTTPClient(&http.Client{
- Transport: &http.Transport{
- TLSClientConfig: &tls.Config{InsecureSkipVerify: setting.Migrations.SkipTLSVerify},
- Proxy: proxy.Proxy(),
- },
- }),
+ gitea_sdk.SetHTTPClient(NewMigrationHTTPClient()),
)
if err != nil {
log.Error(fmt.Sprintf("Failed to create NewGiteaDownloader for: %s. Error: %v", baseURL, err))
@@ -275,12 +267,7 @@ func (g *GiteaDownloader) convertGiteaRelease(rel *gitea_sdk.Release) *base.Rele
Created: rel.CreatedAt,
}
- httpClient := &http.Client{
- Transport: &http.Transport{
- TLSClientConfig: &tls.Config{InsecureSkipVerify: setting.Migrations.SkipTLSVerify},
- Proxy: proxy.Proxy(),
- },
- }
+ httpClient := NewMigrationHTTPClient()
for _, asset := range rel.Attachments {
size := int(asset.Size)
diff --git a/services/migrations/gitea_uploader.go b/services/migrations/gitea_uploader.go
index 2394c63d76..d28c83a64f 100644
--- a/services/migrations/gitea_uploader.go
+++ b/services/migrations/gitea_uploader.go
@@ -125,7 +125,7 @@ func (g *GiteaLocalUploader) CreateRepo(repo *base.Repository, opts base.Migrate
Wiki: opts.Wiki,
Releases: opts.Releases, // if didn't get releases, then sync them from tags
MirrorInterval: opts.MirrorInterval,
- })
+ }, NewMigrationHTTPTransport())
g.repo = r
if err != nil {
diff --git a/services/migrations/github.go b/services/migrations/github.go
index 3043d7cf75..b360b05061 100644
--- a/services/migrations/github.go
+++ b/services/migrations/github.go
@@ -7,7 +7,6 @@ package migrations
import (
"context"
- "crypto/tls"
"fmt"
"io"
"net/http"
@@ -19,7 +18,6 @@ import (
"code.gitea.io/gitea/modules/log"
base "code.gitea.io/gitea/modules/migration"
"code.gitea.io/gitea/modules/proxy"
- "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
@@ -100,12 +98,7 @@ func NewGithubDownloaderV3(ctx context.Context, baseURL, userName, password, tok
)
var client = &http.Client{
Transport: &oauth2.Transport{
- Base: &http.Transport{
- TLSClientConfig: &tls.Config{InsecureSkipVerify: setting.Migrations.SkipTLSVerify},
- Proxy: func(req *http.Request) (*url.URL, error) {
- return proxy.Proxy()(req)
- },
- },
+ Base: NewMigrationHTTPTransport(),
Source: oauth2.ReuseTokenSource(nil, ts),
},
}
@@ -113,14 +106,13 @@ func NewGithubDownloaderV3(ctx context.Context, baseURL, userName, password, tok
downloader.addClient(client, baseURL)
}
} else {
+ var transport = NewMigrationHTTPTransport()
+ transport.Proxy = func(req *http.Request) (*url.URL, error) {
+ req.SetBasicAuth(userName, password)
+ return proxy.Proxy()(req)
+ }
var client = &http.Client{
- Transport: &http.Transport{
- TLSClientConfig: &tls.Config{InsecureSkipVerify: setting.Migrations.SkipTLSVerify},
- Proxy: func(req *http.Request) (*url.URL, error) {
- req.SetBasicAuth(userName, password)
- return proxy.Proxy()(req)
- },
- },
+ Transport: transport,
}
downloader.addClient(client, baseURL)
}
@@ -316,12 +308,7 @@ func (g *GithubDownloaderV3) convertGithubRelease(rel *github.RepositoryRelease)
r.Published = rel.PublishedAt.Time
}
- httpClient := &http.Client{
- Transport: &http.Transport{
- TLSClientConfig: &tls.Config{InsecureSkipVerify: setting.Migrations.SkipTLSVerify},
- Proxy: proxy.Proxy(),
- },
- }
+ httpClient := NewMigrationHTTPClient()
for _, asset := range rel.Assets {
var assetID = *asset.ID // Don't optimize this, for closure we need a local variable
diff --git a/services/migrations/gitlab.go b/services/migrations/gitlab.go
index e285519aa5..4eb7e3e47c 100644
--- a/services/migrations/gitlab.go
+++ b/services/migrations/gitlab.go
@@ -6,7 +6,6 @@ package migrations
import (
"context"
- "crypto/tls"
"errors"
"fmt"
"io"
@@ -18,8 +17,6 @@ import (
"code.gitea.io/gitea/modules/log"
base "code.gitea.io/gitea/modules/migration"
- "code.gitea.io/gitea/modules/proxy"
- "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
"github.com/xanzy/go-gitlab"
@@ -77,16 +74,11 @@ type GitlabDownloader struct {
// Use either a username/password, personal token entered into the username field, or anonymous/public access
// Note: Public access only allows very basic access
func NewGitlabDownloader(ctx context.Context, baseURL, repoPath, username, password, token string) (*GitlabDownloader, error) {
- gitlabClient, err := gitlab.NewClient(token, gitlab.WithBaseURL(baseURL), gitlab.WithHTTPClient(&http.Client{
- Transport: &http.Transport{
- TLSClientConfig: &tls.Config{InsecureSkipVerify: setting.Migrations.SkipTLSVerify},
- Proxy: proxy.Proxy(),
- },
- }))
+ gitlabClient, err := gitlab.NewClient(token, gitlab.WithBaseURL(baseURL), gitlab.WithHTTPClient(NewMigrationHTTPClient()))
// Only use basic auth if token is blank and password is NOT
// Basic auth will fail with empty strings, but empty token will allow anonymous public API usage
if token == "" && password != "" {
- gitlabClient, err = gitlab.NewBasicAuthClient(username, password, gitlab.WithBaseURL(baseURL))
+ gitlabClient, err = gitlab.NewBasicAuthClient(username, password, gitlab.WithBaseURL(baseURL), gitlab.WithHTTPClient(NewMigrationHTTPClient()))
}
if err != nil {
@@ -300,12 +292,7 @@ func (g *GitlabDownloader) convertGitlabRelease(rel *gitlab.Release) *base.Relea
PublisherName: rel.Author.Username,
}
- httpClient := &http.Client{
- Transport: &http.Transport{
- TLSClientConfig: &tls.Config{InsecureSkipVerify: setting.Migrations.SkipTLSVerify},
- Proxy: proxy.Proxy(),
- },
- }
+ httpClient := NewMigrationHTTPClient()
for k, asset := range rel.Assets.Links {
r.Assets = append(r.Assets, &base.ReleaseAsset{
diff --git a/services/migrations/gogs.go b/services/migrations/gogs.go
index 9473bf8b48..5a477c542d 100644
--- a/services/migrations/gogs.go
+++ b/services/migrations/gogs.go
@@ -6,7 +6,6 @@ package migrations
import (
"context"
- "crypto/tls"
"fmt"
"net/http"
"net/url"
@@ -16,7 +15,6 @@ import (
"code.gitea.io/gitea/modules/log"
base "code.gitea.io/gitea/modules/migration"
"code.gitea.io/gitea/modules/proxy"
- "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
"github.com/gogs/go-gogs-client"
@@ -97,13 +95,12 @@ func NewGogsDownloader(ctx context.Context, baseURL, userName, password, token,
client = gogs.NewClient(baseURL, token)
downloader.userName = token
} else {
- downloader.transport = &http.Transport{
- TLSClientConfig: &tls.Config{InsecureSkipVerify: setting.Migrations.SkipTLSVerify},
- Proxy: func(req *http.Request) (*url.URL, error) {
- req.SetBasicAuth(userName, password)
- return proxy.Proxy()(req)
- },
+ var transport = NewMigrationHTTPTransport()
+ transport.Proxy = func(req *http.Request) (*url.URL, error) {
+ req.SetBasicAuth(userName, password)
+ return proxy.Proxy()(req)
}
+ downloader.transport = transport
client = gogs.NewClient(baseURL, "")
client.SetHTTPClient(&http.Client{
diff --git a/services/migrations/http_client.go b/services/migrations/http_client.go
new file mode 100644
index 0000000000..0d683693a1
--- /dev/null
+++ b/services/migrations/http_client.go
@@ -0,0 +1,30 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package migrations
+
+import (
+ "crypto/tls"
+ "net/http"
+
+ "code.gitea.io/gitea/modules/hostmatcher"
+ "code.gitea.io/gitea/modules/proxy"
+ "code.gitea.io/gitea/modules/setting"
+)
+
+// NewMigrationHTTPClient returns a HTTP client for migration
+func NewMigrationHTTPClient() *http.Client {
+ return &http.Client{
+ Transport: NewMigrationHTTPTransport(),
+ }
+}
+
+// NewMigrationHTTPTransport returns a HTTP transport for migration
+func NewMigrationHTTPTransport() *http.Transport {
+ return &http.Transport{
+ TLSClientConfig: &tls.Config{InsecureSkipVerify: setting.Migrations.SkipTLSVerify},
+ Proxy: proxy.Proxy(),
+ DialContext: hostmatcher.NewDialContext("migration", allowList, blockList),
+ }
+}
diff --git a/services/migrations/migrate.go b/services/migrations/migrate.go
index e6d5e8e4cc..3e805f0b71 100644
--- a/services/migrations/migrate.go
+++ b/services/migrations/migrate.go
@@ -15,8 +15,8 @@ import (
"code.gitea.io/gitea/models"
admin_model "code.gitea.io/gitea/models/admin"
+ "code.gitea.io/gitea/modules/hostmatcher"
"code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/matchlist"
base "code.gitea.io/gitea/modules/migration"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
@@ -28,8 +28,8 @@ type MigrateOptions = base.MigrateOptions
var (
factories []base.DownloaderFactory
- allowList *matchlist.Matchlist
- blockList *matchlist.Matchlist
+ allowList *hostmatcher.HostMatchList
+ blockList *hostmatcher.HostMatchList
)
// RegisterDownloaderFactory registers a downloader factory
@@ -73,30 +73,35 @@ func IsMigrateURLAllowed(remoteURL string, doer *models.User) error {
return &models.ErrInvalidCloneAddr{Host: u.Host, IsProtocolInvalid: true, IsPermissionDenied: true, IsURLError: true}
}
- host := strings.ToLower(u.Host)
- if len(setting.Migrations.AllowedDomains) > 0 {
- if !allowList.Match(host) {
- return &models.ErrInvalidCloneAddr{Host: u.Host, IsPermissionDenied: true}
- }
- } else {
- if blockList.Match(host) {
- return &models.ErrInvalidCloneAddr{Host: u.Host, IsPermissionDenied: true}
- }
+ hostName, _, err := net.SplitHostPort(u.Host)
+ if err != nil {
+ // u.Host can be "host" or "host:port"
+ err = nil //nolint
+ hostName = u.Host
+ }
+ addrList, err := net.LookupIP(hostName)
+ if err != nil {
+ return &models.ErrInvalidCloneAddr{Host: u.Host, NotResolvedIP: true}
}
- if !setting.Migrations.AllowLocalNetworks {
- addrList, err := net.LookupIP(strings.Split(u.Host, ":")[0])
- if err != nil {
- return &models.ErrInvalidCloneAddr{Host: u.Host, NotResolvedIP: true}
- }
- for _, addr := range addrList {
- if util.IsIPPrivate(addr) || !addr.IsGlobalUnicast() {
- return &models.ErrInvalidCloneAddr{Host: u.Host, PrivateNet: addr.String(), IsPermissionDenied: true}
- }
+ var ipAllowed bool
+ var ipBlocked bool
+ for _, addr := range addrList {
+ ipAllowed = ipAllowed || allowList.MatchIPAddr(addr)
+ ipBlocked = ipBlocked || blockList.MatchIPAddr(addr)
+ }
+ var blockedError error
+ if blockList.MatchHostName(hostName) || ipBlocked {
+ blockedError = &models.ErrInvalidCloneAddr{Host: u.Host, IsPermissionDenied: true}
+ }
+ // if we have an allow-list, check the allow-list first
+ if !allowList.IsEmpty() {
+ if !allowList.MatchHostName(hostName) && !ipAllowed {
+ return &models.ErrInvalidCloneAddr{Host: u.Host, IsPermissionDenied: true}
}
}
-
- return nil
+ // otherwise, we always follow the blocked list
+ return blockedError
}
// MigrateRepository migrate repository according MigrateOptions
@@ -462,16 +467,18 @@ func migrateRepository(downloader base.Downloader, uploader base.Uploader, opts
// Init migrations service
func Init() error {
- var err error
- allowList, err = matchlist.NewMatchlist(setting.Migrations.AllowedDomains...)
- if err != nil {
- return fmt.Errorf("init migration allowList domains failed: %v", err)
- }
+ // TODO: maybe we can deprecate these legacy ALLOWED_DOMAINS/ALLOW_LOCALNETWORKS/BLOCKED_DOMAINS, use ALLOWED_HOST_LIST/BLOCKED_HOST_LIST instead
- blockList, err = matchlist.NewMatchlist(setting.Migrations.BlockedDomains...)
- if err != nil {
- return fmt.Errorf("init migration blockList domains failed: %v", err)
- }
+ blockList = hostmatcher.ParseSimpleMatchList("migrations.BLOCKED_DOMAINS", setting.Migrations.BlockedDomains)
+ allowList = hostmatcher.ParseSimpleMatchList("migrations.ALLOWED_DOMAINS/ALLOW_LOCALNETWORKS", setting.Migrations.AllowedDomains)
+ if allowList.IsEmpty() {
+ // the default policy is that migration module can access external hosts
+ allowList.AppendBuiltin(hostmatcher.MatchBuiltinExternal)
+ }
+ if setting.Migrations.AllowLocalNetworks {
+ allowList.AppendBuiltin(hostmatcher.MatchBuiltinPrivate)
+ allowList.AppendBuiltin(hostmatcher.MatchBuiltinLoopback)
+ }
return nil
}
diff --git a/services/migrations/migrate_test.go b/services/migrations/migrate_test.go
index 325064697e..e2363242a2 100644
--- a/services/migrations/migrate_test.go
+++ b/services/migrations/migrate_test.go
@@ -21,7 +21,8 @@ func TestMigrateWhiteBlocklist(t *testing.T) {
adminUser := unittest.AssertExistsAndLoadBean(t, &models.User{Name: "user1"}).(*models.User)
nonAdminUser := unittest.AssertExistsAndLoadBean(t, &models.User{Name: "user2"}).(*models.User)
- setting.Migrations.AllowedDomains = []string{"github.com"}
+ setting.Migrations.AllowedDomains = "github.com"
+ setting.Migrations.AllowLocalNetworks = false
assert.NoError(t, Init())
err := IsMigrateURLAllowed("https://gitlab.com/gitlab/gitlab.git", nonAdminUser)
@@ -33,8 +34,8 @@ func TestMigrateWhiteBlocklist(t *testing.T) {
err = IsMigrateURLAllowed("https://gITHUb.com/go-gitea/gitea.git", nonAdminUser)
assert.NoError(t, err)
- setting.Migrations.AllowedDomains = []string{}
- setting.Migrations.BlockedDomains = []string{"github.com"}
+ setting.Migrations.AllowedDomains = ""
+ setting.Migrations.BlockedDomains = "github.com"
assert.NoError(t, Init())
err = IsMigrateURLAllowed("https://gitlab.com/gitlab/gitlab.git", nonAdminUser)
@@ -47,6 +48,7 @@ func TestMigrateWhiteBlocklist(t *testing.T) {
assert.Error(t, err)
setting.Migrations.AllowLocalNetworks = true
+ assert.NoError(t, Init())
err = IsMigrateURLAllowed("https://10.0.0.1/go-gitea/gitea.git", nonAdminUser)
assert.NoError(t, err)