Skip to content

Commit e949447

Browse files
committed
use hostmatcher to replace matchlist, improve security
1 parent edbaa5d commit e949447

File tree

27 files changed

+346
-279
lines changed

27 files changed

+346
-279
lines changed

integrations/mirror_pull_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ func TestMirrorPull(t *testing.T) {
4747

4848
ctx := context.Background()
4949

50-
mirror, err := repository.MigrateRepositoryGitData(ctx, user, mirrorRepo, opts)
50+
mirror, err := repository.MigrateRepositoryGitData(ctx, user, mirrorRepo, opts, nil)
5151
assert.NoError(t, err)
5252

5353
gitRepo, err := git.OpenRepository(repoPath)

models/error.go

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -909,7 +909,6 @@ type ErrInvalidCloneAddr struct {
909909
IsPermissionDenied bool
910910
LocalPath bool
911911
NotResolvedIP bool
912-
PrivateNet string
913912
}
914913

915914
// IsErrInvalidCloneAddr checks if an error is a ErrInvalidCloneAddr.
@@ -922,9 +921,6 @@ func (err *ErrInvalidCloneAddr) Error() string {
922921
if err.NotResolvedIP {
923922
return fmt.Sprintf("migration/cloning from '%s' is not allowed: unknown hostname", err.Host)
924923
}
925-
if len(err.PrivateNet) != 0 {
926-
return fmt.Sprintf("migration/cloning from '%s' is not allowed: the host resolve to a private ip address '%s'", err.Host, err.PrivateNet)
927-
}
928924
if err.IsInvalidPath {
929925
return fmt.Sprintf("migration/cloning from '%s' is not allowed: the provided path is invalid", err.Host)
930926
}

modules/hostmatcher/hostmatcher.go

Lines changed: 102 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package hostmatcher
66

77
import (
8+
"fmt"
89
"net"
910
"path/filepath"
1011
"strings"
@@ -15,7 +16,11 @@ import (
1516
// HostMatchList is used to check if a host or IP is in a list.
1617
// If you only need to do wildcard matching, consider to use modules/matchlist
1718
type HostMatchList struct {
18-
hosts []string
19+
SettingKeyHint string
20+
SettingValue string
21+
22+
// host name or built-in network name
23+
names []string
1924
ipNets []*net.IPNet
2025
}
2126

@@ -32,8 +37,8 @@ const MatchBuiltinPrivate = "private"
3237
const MatchBuiltinLoopback = "loopback"
3338

3439
// ParseHostMatchList parses the host list HostMatchList
35-
func ParseHostMatchList(hostList string) *HostMatchList {
36-
hl := &HostMatchList{}
40+
func ParseHostMatchList(settingKeyHint string, hostList string) *HostMatchList {
41+
hl := &HostMatchList{SettingKeyHint: settingKeyHint, SettingValue: hostList}
3742
for _, s := range strings.Split(hostList, ",") {
3843
s = strings.ToLower(strings.TrimSpace(s))
3944
if s == "" {
@@ -43,52 +48,116 @@ func ParseHostMatchList(hostList string) *HostMatchList {
4348
if err == nil {
4449
hl.ipNets = append(hl.ipNets, ipNet)
4550
} else {
46-
hl.hosts = append(hl.hosts, s)
51+
hl.names = append(hl.names, s)
4752
}
4853
}
4954
return hl
5055
}
5156

52-
// MatchesHostOrIP checks if the host or IP matches an allow/deny(block) list
53-
func (hl *HostMatchList) MatchesHostOrIP(host string, ip net.IP) bool {
54-
var matched bool
55-
host = strings.ToLower(host)
56-
ipStr := ip.String()
57-
loop:
58-
for _, hostInList := range hl.hosts {
59-
switch hostInList {
57+
// ParseSimpleMatchList parse a simple matchlist (no built-in networks, no CIDR support)
58+
func ParseSimpleMatchList(settingKeyHint string, matchList string, includeLocalNetwork bool) *HostMatchList {
59+
hl := &HostMatchList{
60+
SettingKeyHint: settingKeyHint,
61+
SettingValue: matchList + fmt.Sprintf("(local-network:%v)", includeLocalNetwork),
62+
}
63+
for _, s := range strings.Split(matchList, ",") {
64+
s = strings.ToLower(strings.TrimSpace(s))
65+
if s == "" {
66+
continue
67+
}
68+
if s == MatchBuiltinLoopback || s == MatchBuiltinPrivate || s == MatchBuiltinExternal {
69+
// for built-in names, we convert it from "private" => "[p]rivate" for internal usage and keep the same result as `matchlist`
70+
hl.names = append(hl.names, "["+s[:1]+"]"+s[1:])
71+
} else {
72+
// we keep the same result as `matchlist`, so no CIDR support here
73+
hl.names = append(hl.names, s)
74+
}
75+
}
76+
if includeLocalNetwork {
77+
hl.names = append(hl.names, MatchBuiltinPrivate)
78+
}
79+
return hl
80+
}
81+
82+
// IsEmpty checks if the check list is empty
83+
func (hl *HostMatchList) IsEmpty() bool {
84+
return hl == nil || (len(hl.names) == 0 && len(hl.ipNets) == 0)
85+
}
86+
87+
func (hl *HostMatchList) checkNames(host string) bool {
88+
host = strings.ToLower(strings.TrimSpace(host))
89+
for _, name := range hl.names {
90+
switch name {
6091
case "":
92+
case MatchBuiltinExternal:
93+
case MatchBuiltinPrivate:
94+
case MatchBuiltinLoopback:
95+
// ignore empty string or built-in network names
6196
continue
6297
case MatchBuiltinAll:
63-
matched = true
64-
break loop
98+
return true
99+
default:
100+
if matched, _ := filepath.Match(name, host); matched {
101+
return true
102+
}
103+
}
104+
}
105+
return false
106+
}
107+
108+
func (hl *HostMatchList) checkIP(ip net.IP) bool {
109+
for _, name := range hl.names {
110+
switch name {
111+
case "":
112+
continue
113+
case MatchBuiltinAll:
114+
return true
65115
case MatchBuiltinExternal:
66-
if matched = ip.IsGlobalUnicast() && !util.IsIPPrivate(ip); matched {
67-
break loop
116+
if ip.IsGlobalUnicast() && !util.IsIPPrivate(ip) {
117+
return true
68118
}
69119
case MatchBuiltinPrivate:
70-
if matched = util.IsIPPrivate(ip); matched {
71-
break loop
120+
if util.IsIPPrivate(ip) {
121+
return true
72122
}
73123
case MatchBuiltinLoopback:
74-
if matched = ip.IsLoopback(); matched {
75-
break loop
76-
}
77-
default:
78-
if matched, _ = filepath.Match(hostInList, host); matched {
79-
break loop
80-
}
81-
if matched, _ = filepath.Match(hostInList, ipStr); matched {
82-
break loop
124+
if ip.IsLoopback() {
125+
return true
83126
}
84127
}
85128
}
86-
if !matched {
87-
for _, ipNet := range hl.ipNets {
88-
if matched = ipNet.Contains(ip); matched {
89-
break
90-
}
129+
for _, ipNet := range hl.ipNets {
130+
if ipNet.Contains(ip) {
131+
return true
91132
}
92133
}
93-
return matched
134+
return false
135+
}
136+
137+
// MatchHostName checks if the host matches an allow/deny(block) list
138+
func (hl *HostMatchList) MatchHostName(host string) bool {
139+
if hl == nil {
140+
return false
141+
}
142+
if hl.checkNames(host) {
143+
return true
144+
}
145+
if ip := net.ParseIP(host); ip != nil {
146+
return hl.checkIP(ip)
147+
}
148+
return false
149+
}
150+
151+
// MatchIPAddr checks if the IP matches an allow/deny(block) list, it's safe to pass `nil` to `ip`
152+
func (hl *HostMatchList) MatchIPAddr(ip net.IP) bool {
153+
if hl == nil {
154+
return false
155+
}
156+
host := ip.String() // nil-safe, we will get "<nil>" if ip is nil
157+
return hl.checkNames(host) || hl.checkIP(ip)
158+
}
159+
160+
// MatchHostOrIP checks if the host or IP matches an allow/deny(block) list
161+
func (hl *HostMatchList) MatchHostOrIP(host string, ip net.IP) bool {
162+
return hl.MatchHostName(host) || hl.MatchIPAddr(ip)
94163
}

modules/hostmatcher/hostmatcher_test.go

Lines changed: 53 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,28 @@ func TestHostOrIPMatchesList(t *testing.T) {
2020

2121
// for IPv6: "::1" is loopback, "fd00::/8" is private
2222

23-
hl := ParseHostMatchList("private, External, *.myDomain.com, 169.254.1.0/24")
23+
hl := ParseHostMatchList("", "private, External, *.myDomain.com, 169.254.1.0/24")
24+
25+
test := func(cases []tc) {
26+
for _, c := range cases {
27+
assert.Equalf(t, c.expected, hl.MatchHostOrIP(c.host, c.ip), "case domain=%s, ip=%v, expected=%v", c.host, c.ip, c.expected)
28+
}
29+
}
30+
2431
cases := []tc{
2532
{"", net.IPv4zero, false},
2633
{"", net.IPv6zero, false},
2734

2835
{"", net.ParseIP("127.0.0.1"), false},
36+
{"127.0.0.1", nil, false},
2937
{"", net.ParseIP("::1"), false},
3038

3139
{"", net.ParseIP("10.0.1.1"), true},
40+
{"10.0.1.1", nil, true},
3241
{"", net.ParseIP("192.168.1.1"), true},
42+
{"192.168.1.1", nil, true},
3343
{"", net.ParseIP("fd00::1"), true},
44+
{"fd00::1", nil, true},
3445

3546
{"", net.ParseIP("8.8.8.8"), true},
3647
{"", net.ParseIP("1001::1"), true},
@@ -39,13 +50,13 @@ func TestHostOrIPMatchesList(t *testing.T) {
3950
{"sub.mydomain.com", net.IPv4zero, true},
4051

4152
{"", net.ParseIP("169.254.1.1"), true},
53+
{"169.254.1.1", nil, true},
4254
{"", net.ParseIP("169.254.2.2"), false},
55+
{"169.254.2.2", nil, false},
4356
}
44-
for _, c := range cases {
45-
assert.Equalf(t, c.expected, hl.MatchesHostOrIP(c.host, c.ip), "case %s(%v)", c.host, c.ip)
46-
}
57+
test(cases)
4758

48-
hl = ParseHostMatchList("loopback")
59+
hl = ParseHostMatchList("", "loopback")
4960
cases = []tc{
5061
{"", net.IPv4zero, false},
5162
{"", net.ParseIP("127.0.0.1"), true},
@@ -59,11 +70,9 @@ func TestHostOrIPMatchesList(t *testing.T) {
5970

6071
{"mydomain.com", net.IPv4zero, false},
6172
}
62-
for _, c := range cases {
63-
assert.Equalf(t, c.expected, hl.MatchesHostOrIP(c.host, c.ip), "case %s(%v)", c.host, c.ip)
64-
}
73+
test(cases)
6574

66-
hl = ParseHostMatchList("private")
75+
hl = ParseHostMatchList("", "private")
6776
cases = []tc{
6877
{"", net.IPv4zero, false},
6978
{"", net.ParseIP("127.0.0.1"), false},
@@ -77,11 +86,9 @@ func TestHostOrIPMatchesList(t *testing.T) {
7786

7887
{"mydomain.com", net.IPv4zero, false},
7988
}
80-
for _, c := range cases {
81-
assert.Equalf(t, c.expected, hl.MatchesHostOrIP(c.host, c.ip), "case %s(%v)", c.host, c.ip)
82-
}
89+
test(cases)
8390

84-
hl = ParseHostMatchList("external")
91+
hl = ParseHostMatchList("", "external")
8592
cases = []tc{
8693
{"", net.IPv4zero, false},
8794
{"", net.ParseIP("127.0.0.1"), false},
@@ -95,11 +102,9 @@ func TestHostOrIPMatchesList(t *testing.T) {
95102

96103
{"mydomain.com", net.IPv4zero, false},
97104
}
98-
for _, c := range cases {
99-
assert.Equalf(t, c.expected, hl.MatchesHostOrIP(c.host, c.ip), "case %s(%v)", c.host, c.ip)
100-
}
105+
test(cases)
101106

102-
hl = ParseHostMatchList("*")
107+
hl = ParseHostMatchList("", "*")
103108
cases = []tc{
104109
{"", net.IPv4zero, true},
105110
{"", net.ParseIP("127.0.0.1"), true},
@@ -113,7 +118,36 @@ func TestHostOrIPMatchesList(t *testing.T) {
113118

114119
{"mydomain.com", net.IPv4zero, true},
115120
}
116-
for _, c := range cases {
117-
assert.Equalf(t, c.expected, hl.MatchesHostOrIP(c.host, c.ip), "case %s(%v)", c.host, c.ip)
121+
test(cases)
122+
123+
// built-in network names can be escaped (warping the first char with `[]`) to be used as a real host name
124+
// this mechanism is reversed for internal usage only (maybe for some rare cases), it's not supposed to be used by end users
125+
// a real user should never use loopback/private/external as their host names
126+
hl = ParseHostMatchList("", "loopback, [p]rivate")
127+
cases = []tc{
128+
{"loopback", nil, false},
129+
{"", net.ParseIP("127.0.0.1"), true},
130+
{"private", nil, true},
131+
{"", net.ParseIP("192.168.1.1"), false},
132+
}
133+
test(cases)
134+
135+
hl = ParseSimpleMatchList("", "loopback, *.domain.com", true)
136+
cases = []tc{
137+
{"loopback", nil, true},
138+
{"", net.ParseIP("127.0.0.1"), false},
139+
{"sub.domain.com", nil, true},
140+
{"other.com", nil, false},
141+
{"", net.ParseIP("192.168.1.1"), true},
142+
{"", net.ParseIP("1.1.1.1"), false},
143+
}
144+
test(cases)
145+
146+
hl = ParseSimpleMatchList("", "external", false)
147+
cases = []tc{
148+
{"", net.ParseIP("192.168.1.1"), false},
149+
{"", net.ParseIP("1.1.1.1"), false},
150+
{"external", nil, true},
118151
}
152+
test(cases)
119153
}

modules/hostmatcher/http.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// Copyright 2021 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package hostmatcher
6+
7+
import (
8+
"context"
9+
"fmt"
10+
"net"
11+
"syscall"
12+
"time"
13+
)
14+
15+
// NewDialContext returns a DialContext for Transport, the DialContext will do allow/block list check
16+
func NewDialContext(usage string, allowList *HostMatchList, blockList *HostMatchList) func(ctx context.Context, network, addr string) (net.Conn, error) {
17+
// How Go HTTP Client works with redirection:
18+
// transport.RoundTrip URL=http://domain.com, Host=domain.com
19+
// transport.DialContext addrOrHost=domain.com:80
20+
// dialer.Control tcp4:11.22.33.44:80
21+
// transport.RoundTrip URL=http://www.domain.com/, Host=(empty here, in the direction, HTTP client doesn't fill the Host field)
22+
// transport.DialContext addrOrHost=domain.com:80
23+
// dialer.Control tcp4:11.22.33.44:80
24+
return func(ctx context.Context, network, addrOrHost string) (net.Conn, error) {
25+
dialer := net.Dialer{
26+
// default values comes from http.DefaultTransport
27+
Timeout: 30 * time.Second,
28+
KeepAlive: 30 * time.Second,
29+
30+
Control: func(network, ipAddr string, c syscall.RawConn) (err error) {
31+
var host string
32+
if host, _, err = net.SplitHostPort(addrOrHost); err != nil {
33+
return err
34+
}
35+
// in Control func, the addr was already resolved to IP:PORT format, there is no cost to do ResolveTCPAddr here
36+
tcpAddr, err := net.ResolveTCPAddr(network, ipAddr)
37+
if err != nil {
38+
return fmt.Errorf("%s can only call HTTP servers via TCP, deny '%s(%s:%s)', err=%v", usage, host, network, ipAddr, err)
39+
}
40+
41+
var blockedError error
42+
if blockList.MatchHostOrIP(host, tcpAddr.IP) {
43+
blockedError = fmt.Errorf("%s can not call blocked HTTP servers (check your %s setting), deny '%s(%s)'", usage, blockList.SettingKeyHint, host, ipAddr)
44+
}
45+
46+
// if we have an allow-list, check the allow-list first
47+
if !allowList.IsEmpty() {
48+
if !allowList.MatchHostOrIP(host, tcpAddr.IP) {
49+
return fmt.Errorf("%s can only call allowed HTTP servers (check your %s setting), deny '%s(%s)'", usage, allowList.SettingKeyHint, host, ipAddr)
50+
}
51+
}
52+
// otherwise, we always follow the blocked list
53+
return blockedError
54+
},
55+
}
56+
return dialer.DialContext(ctx, network, addrOrHost)
57+
}
58+
}

0 commit comments

Comments
 (0)