forked from forgejo/forgejo
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.
This commit is contained in:
parent
c96be0cd98
commit
013fb73068
33 changed files with 377 additions and 293 deletions
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -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{
|
||||
|
|
30
services/migrations/http_client.go
Normal file
30
services/migrations/http_client.go
Normal file
|
@ -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),
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
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}
|
||||
}
|
||||
|
||||
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}
|
||||
}
|
||||
}
|
||||
|
||||
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}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue