Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use hostmatcher to replace matchlist to improve blocking of bad hosts in Webhooks #17605

Merged
merged 28 commits into from
Nov 20, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
e949447
use hostmatcher to replace matchlist, improve security
wxiaoguang Nov 10, 2021
622e594
Merge branch 'main' into refactor-matchlist
wxiaoguang Nov 10, 2021
c1c021f
fix unit test
wxiaoguang Nov 10, 2021
ba3e16c
fix unit test
wxiaoguang Nov 10, 2021
af2cb2c
fix unit test
wxiaoguang Nov 10, 2021
58c0171
Merge branch 'main' into refactor-matchlist
6543 Nov 10, 2021
91090fc
Merge branch 'main' into refactor-matchlist
wxiaoguang Nov 11, 2021
696a4e9
refactor
wxiaoguang Nov 12, 2021
27060b6
Merge branch 'refactor-matchlist' of github.com:wxiaoguang/gitea into…
wxiaoguang Nov 12, 2021
0a993c0
Merge branch 'main' into refactor-matchlist
wxiaoguang Nov 12, 2021
5b17f23
fix simple matchlist
wxiaoguang Nov 12, 2021
aeff3d4
Merge branch 'main' into refactor-matchlist
wxiaoguang Nov 12, 2021
65a8845
Merge branch 'main' into refactor-matchlist
wxiaoguang Nov 13, 2021
435415c
Merge remote-tracking branch 'go-gitea/main' into refactor-matchlist
wxiaoguang Nov 15, 2021
8713da3
Merge branch 'main' into refactor-matchlist
wxiaoguang Nov 15, 2021
dbdfe98
Merge branch 'main' into refactor-matchlist
wxiaoguang Nov 17, 2021
8a7ea65
fix merge conflicts and unit test
wxiaoguang Nov 17, 2021
5f947d8
Merge branch 'main' into refactor-matchlist
wxiaoguang Nov 17, 2021
6a23279
Merge branch 'main' into refactor-matchlist
wxiaoguang Nov 18, 2021
ff56fb6
fix lint
wxiaoguang Nov 18, 2021
cdb4932
Merge branch 'main' into refactor-matchlist
wxiaoguang Nov 19, 2021
5ca9912
use `webhookHTTPClient.Do(req.WithContext(graceful.GetManager().Shutd…
wxiaoguang Nov 19, 2021
fa15ec1
Merge branch 'main' into refactor-matchlist
wxiaoguang Nov 19, 2021
abfbfb5
Merge branch 'main' into refactor-matchlist
wxiaoguang Nov 19, 2021
ab9cc75
Merge branch 'main' into refactor-matchlist
wxiaoguang Nov 19, 2021
fc3db84
Merge branch 'main' into refactor-matchlist
wxiaoguang Nov 19, 2021
692f121
fix documents
wxiaoguang Nov 19, 2021
e690a9f
Merge branch 'main' into refactor-matchlist
lunny Nov 20, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion custom/conf/app.example.ini
Original file line number Diff line number Diff line change
Expand Up @@ -2114,7 +2114,7 @@ PATH =
;ALLOWED_DOMAINS =
;;
;; Blocklist for migrating, default is blank. Multiple domains could be separated by commas.
;; When ALLOWED_DOMAINS is not blank, this option will be ignored.
;; When ALLOWED_DOMAINS is not blank, this option has a higher priority to deny domains.
;BLOCKED_DOMAINS =
;;
;; Allow private addresses defined by RFC 1918, RFC 1122, RFC 4632 and RFC 4291 (false by default)
Expand Down
2 changes: 1 addition & 1 deletion docs/content/doc/advanced/config-cheat-sheet.en-us.md
Original file line number Diff line number Diff line change
Expand Up @@ -1045,7 +1045,7 @@ Task queue configuration has been moved to `queue.task`. However, the below conf
- `MAX_ATTEMPTS`: **3**: Max attempts per http/https request on migrations.
- `RETRY_BACKOFF`: **3**: Backoff time per http/https request retry (seconds)
- `ALLOWED_DOMAINS`: **\<empty\>**: Domains allowlist for migrating repositories, default is blank. It means everything will be allowed. Multiple domains could be separated by commas.
- `BLOCKED_DOMAINS`: **\<empty\>**: Domains blocklist for migrating repositories, default is blank. Multiple domains could be separated by commas. When `ALLOWED_DOMAINS` is not blank, this option will be ignored.
- `BLOCKED_DOMAINS`: **\<empty\>**: Domains blocklist for migrating repositories, default is blank. Multiple domains could be separated by commas. When `ALLOWED_DOMAINS` is not blank, this option has a higher priority to deny domains.
- `ALLOW_LOCALNETWORKS`: **false**: Allow private addresses defined by RFC 1918, RFC 1122, RFC 4632 and RFC 4291
- `SKIP_TLS_VERIFY`: **false**: Allow skip tls verify

Expand Down
2 changes: 1 addition & 1 deletion docs/content/doc/advanced/config-cheat-sheet.zh-cn.md
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,7 @@ IS_INPUT_FILE = false
- `MAX_ATTEMPTS`: **3**: 在迁移过程中的 http/https 请求重试次数。
- `RETRY_BACKOFF`: **3**: 等待下一次重试的时间,单位秒。
- `ALLOWED_DOMAINS`: **\<empty\>**: 迁移仓库的域名白名单,默认为空,表示允许从任意域名迁移仓库,多个域名用逗号分隔。
- `BLOCKED_DOMAINS`: **\<empty\>**: 迁移仓库的域名黑名单,默认为空,多个域名用逗号分隔。如果 `ALLOWED_DOMAINS` 不为空,此选项将会被忽略
- `BLOCKED_DOMAINS`: **\<empty\>**: 迁移仓库的域名黑名单,默认为空,多个域名用逗号分隔。如果 `ALLOWED_DOMAINS` 不为空,此选项有更高的优先级拒绝这里的域名
- `ALLOW_LOCALNETWORKS`: **false**: Allow private addresses defined by RFC 1918
- `SKIP_TLS_VERIFY`: **false**: 允许忽略 TLS 认证

Expand Down
3 changes: 3 additions & 0 deletions integrations/api_repo_lfs_migrate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"code.gitea.io/gitea/modules/lfs"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/services/migrations"

"github.com/stretchr/testify/assert"
)
Expand All @@ -25,6 +26,7 @@ func TestAPIRepoLFSMigrateLocal(t *testing.T) {
oldAllowLocalNetworks := setting.Migrations.AllowLocalNetworks
setting.ImportLocalPaths = true
setting.Migrations.AllowLocalNetworks = true
assert.NoError(t, migrations.Init())

user := unittest.AssertExistsAndLoadBean(t, &models.User{ID: 1}).(*models.User)
session := loginUser(t, user.Name)
Expand All @@ -47,4 +49,5 @@ func TestAPIRepoLFSMigrateLocal(t *testing.T) {

setting.ImportLocalPaths = oldImportLocalPaths
setting.Migrations.AllowLocalNetworks = oldAllowLocalNetworks
assert.NoError(t, migrations.Init()) // reset old migration settings
}
4 changes: 2 additions & 2 deletions integrations/api_repo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -331,10 +331,10 @@ func TestAPIRepoMigrate(t *testing.T) {
switch respJSON["message"] {
case "Remote visit addressed rate limitation.":
t.Log("test hit github rate limitation")
case "You are not allowed to import from private IPs.":
case "You can not import from disallowed hosts.":
assert.EqualValues(t, "private-ip", testCase.repoName)
default:
t.Errorf("unexpected error '%v' on url '%s'", respJSON["message"], testCase.cloneURL)
assert.Fail(t, "unexpected error '%v' on url '%s'", respJSON["message"], testCase.cloneURL)
}
} else {
assert.EqualValues(t, testCase.expectedStatus, resp.Code)
Expand Down
2 changes: 1 addition & 1 deletion integrations/mirror_pull_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ func TestMirrorPull(t *testing.T) {

ctx := context.Background()

mirror, err := repository.MigrateRepositoryGitData(ctx, user, mirrorRepo, opts)
mirror, err := repository.MigrateRepositoryGitData(ctx, user, mirrorRepo, opts, nil)
assert.NoError(t, err)

gitRepo, err := git.OpenRepository(repoPath)
Expand Down
2 changes: 2 additions & 0 deletions integrations/mirror_push_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/services/migrations"
mirror_service "code.gitea.io/gitea/services/mirror"

"github.com/stretchr/testify/assert"
Expand All @@ -29,6 +30,7 @@ func testMirrorPush(t *testing.T, u *url.URL) {
defer prepareTestEnv(t)()

setting.Migrations.AllowLocalNetworks = true
assert.NoError(t, migrations.Init())

user := unittest.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User)
srcRepo := unittest.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository)
Expand Down
4 changes: 0 additions & 4 deletions models/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -797,7 +797,6 @@ type ErrInvalidCloneAddr struct {
IsPermissionDenied bool
LocalPath bool
NotResolvedIP bool
PrivateNet string
}

// IsErrInvalidCloneAddr checks if an error is a ErrInvalidCloneAddr.
Expand All @@ -810,9 +809,6 @@ func (err *ErrInvalidCloneAddr) Error() string {
if err.NotResolvedIP {
return fmt.Sprintf("migration/cloning from '%s' is not allowed: unknown hostname", err.Host)
}
if len(err.PrivateNet) != 0 {
return fmt.Sprintf("migration/cloning from '%s' is not allowed: the host resolve to a private ip address '%s'", err.Host, err.PrivateNet)
}
if err.IsInvalidPath {
return fmt.Sprintf("migration/cloning from '%s' is not allowed: the provided path is invalid", err.Host)
}
Expand Down
138 changes: 99 additions & 39 deletions modules/hostmatcher/hostmatcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,18 @@ import (
)

// HostMatchList is used to check if a host or IP is in a list.
// If you only need to do wildcard matching, consider to use modules/matchlist
type HostMatchList struct {
hosts []string
SettingKeyHint string
SettingValue string

// builtins networks
builtins []string
// patterns for host names (with wildcard support)
patterns []string
// ipNets is the CIDR network list
ipNets []*net.IPNet
}

// MatchBuiltinAll all hosts are matched
const MatchBuiltinAll = "*"

// MatchBuiltinExternal A valid non-private unicast IP, all hosts on public internet are matched
const MatchBuiltinExternal = "external"

Expand All @@ -31,9 +34,13 @@ const MatchBuiltinPrivate = "private"
// MatchBuiltinLoopback 127.0.0.0/8 for IPv4 and ::1/128 for IPv6, localhost is included.
const MatchBuiltinLoopback = "loopback"

func isBuiltin(s string) bool {
return s == MatchBuiltinExternal || s == MatchBuiltinPrivate || s == MatchBuiltinLoopback
}

// ParseHostMatchList parses the host list HostMatchList
func ParseHostMatchList(hostList string) *HostMatchList {
hl := &HostMatchList{}
func ParseHostMatchList(settingKeyHint string, hostList string) *HostMatchList {
hl := &HostMatchList{SettingKeyHint: settingKeyHint, SettingValue: hostList}
for _, s := range strings.Split(hostList, ",") {
s = strings.ToLower(strings.TrimSpace(s))
if s == "" {
Expand All @@ -42,53 +49,106 @@ func ParseHostMatchList(hostList string) *HostMatchList {
_, ipNet, err := net.ParseCIDR(s)
if err == nil {
hl.ipNets = append(hl.ipNets, ipNet)
} else if isBuiltin(s) {
hl.builtins = append(hl.builtins, s)
} else {
hl.hosts = append(hl.hosts, s)
hl.patterns = append(hl.patterns, s)
}
}
return hl
}

// MatchesHostOrIP checks if the host or IP matches an allow/deny(block) list
func (hl *HostMatchList) MatchesHostOrIP(host string, ip net.IP) bool {
var matched bool
host = strings.ToLower(host)
ipStr := ip.String()
loop:
for _, hostInList := range hl.hosts {
switch hostInList {
case "":
// ParseSimpleMatchList parse a simple matchlist (no built-in networks, no CIDR support, only wildcard pattern match)
func ParseSimpleMatchList(settingKeyHint string, matchList string) *HostMatchList {
hl := &HostMatchList{
SettingKeyHint: settingKeyHint,
SettingValue: matchList,
}
for _, s := range strings.Split(matchList, ",") {
s = strings.ToLower(strings.TrimSpace(s))
if s == "" {
continue
case MatchBuiltinAll:
matched = true
break loop
}
// we keep the same result as old `matchlist`, so no builtin/CIDR support here, we only match wildcard patterns
hl.patterns = append(hl.patterns, s)
}
return hl
}

// AppendBuiltin appends more builtins to match
func (hl *HostMatchList) AppendBuiltin(builtin string) {
hl.builtins = append(hl.builtins, builtin)
}

// IsEmpty checks if the checklist is empty
func (hl *HostMatchList) IsEmpty() bool {
return hl == nil || (len(hl.builtins) == 0 && len(hl.patterns) == 0 && len(hl.ipNets) == 0)
}

func (hl *HostMatchList) checkPattern(host string) bool {
host = strings.ToLower(strings.TrimSpace(host))
for _, pattern := range hl.patterns {
if matched, _ := filepath.Match(pattern, host); matched {
return true
}
}
return false
}

func (hl *HostMatchList) checkIP(ip net.IP) bool {
for _, pattern := range hl.patterns {
if pattern == "*" {
return true
}
}
for _, builtin := range hl.builtins {
switch builtin {
case MatchBuiltinExternal:
if matched = ip.IsGlobalUnicast() && !util.IsIPPrivate(ip); matched {
break loop
if ip.IsGlobalUnicast() && !util.IsIPPrivate(ip) {
return true
}
case MatchBuiltinPrivate:
if matched = util.IsIPPrivate(ip); matched {
break loop
if util.IsIPPrivate(ip) {
return true
}
case MatchBuiltinLoopback:
if matched = ip.IsLoopback(); matched {
break loop
}
default:
if matched, _ = filepath.Match(hostInList, host); matched {
break loop
}
if matched, _ = filepath.Match(hostInList, ipStr); matched {
break loop
if ip.IsLoopback() {
return true
}
}
}
if !matched {
for _, ipNet := range hl.ipNets {
if matched = ipNet.Contains(ip); matched {
break
}
for _, ipNet := range hl.ipNets {
if ipNet.Contains(ip) {
return true
}
}
return matched
return false
}

// MatchHostName checks if the host matches an allow/deny(block) list
func (hl *HostMatchList) MatchHostName(host string) bool {
if hl == nil {
return false
}
if hl.checkPattern(host) {
return true
}
if ip := net.ParseIP(host); ip != nil {
return hl.checkIP(ip)
}
return false
}

// MatchIPAddr checks if the IP matches an allow/deny(block) list, it's safe to pass `nil` to `ip`
func (hl *HostMatchList) MatchIPAddr(ip net.IP) bool {
if hl == nil {
return false
}
host := ip.String() // nil-safe, we will get "<nil>" if ip is nil
return hl.checkPattern(host) || hl.checkIP(ip)
}

// MatchHostOrIP checks if the host or IP matches an allow/deny(block) list
func (hl *HostMatchList) MatchHostOrIP(host string, ip net.IP) bool {
return hl.MatchHostName(host) || hl.MatchIPAddr(ip)
}
Loading