Only allow webhook to send requests to allowed hosts (#17482)
parent
4e8a81780e
commit
599ff1c054
@ -0,0 +1,94 @@ |
||||
// 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 hostmatcher |
||||
|
||||
import ( |
||||
"net" |
||||
"path/filepath" |
||||
"strings" |
||||
|
||||
"code.gitea.io/gitea/modules/util" |
||||
) |
||||
|
||||
// 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 |
||||
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" |
||||
|
||||
// MatchBuiltinPrivate RFC 1918 (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16) and RFC 4193 (FC00::/7). Also called LAN/Intranet.
|
||||
const MatchBuiltinPrivate = "private" |
||||
|
||||
// MatchBuiltinLoopback 127.0.0.0/8 for IPv4 and ::1/128 for IPv6, localhost is included.
|
||||
const MatchBuiltinLoopback = "loopback" |
||||
|
||||
// ParseHostMatchList parses the host list HostMatchList
|
||||
func ParseHostMatchList(hostList string) *HostMatchList { |
||||
hl := &HostMatchList{} |
||||
for _, s := range strings.Split(hostList, ",") { |
||||
s = strings.ToLower(strings.TrimSpace(s)) |
||||
if s == "" { |
||||
continue |
||||
} |
||||
_, ipNet, err := net.ParseCIDR(s) |
||||
if err == nil { |
||||
hl.ipNets = append(hl.ipNets, ipNet) |
||||
} else { |
||||
hl.hosts = append(hl.hosts, 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 "": |
||||
continue |
||||
case MatchBuiltinAll: |
||||
matched = true |
||||
break loop |
||||
case MatchBuiltinExternal: |
||||
if matched = ip.IsGlobalUnicast() && !util.IsIPPrivate(ip); matched { |
||||
break loop |
||||
} |
||||
case MatchBuiltinPrivate: |
||||
if matched = util.IsIPPrivate(ip); matched { |
||||
break loop |
||||
} |
||||
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 !matched { |
||||
for _, ipNet := range hl.ipNets { |
||||
if matched = ipNet.Contains(ip); matched { |
||||
break |
||||
} |
||||
} |
||||
} |
||||
return matched |
||||
} |
@ -0,0 +1,119 @@ |
||||
// 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 hostmatcher |
||||
|
||||
import ( |
||||
"net" |
||||
"testing" |
||||
|
||||
"github.com/stretchr/testify/assert" |
||||
) |
||||
|
||||
func TestHostOrIPMatchesList(t *testing.T) { |
||||
type tc struct { |
||||
host string |
||||
ip net.IP |
||||
expected bool |
||||
} |
||||
|
||||
// for IPv6: "::1" is loopback, "fd00::/8" is private
|
||||
|
||||
hl := ParseHostMatchList("private, External, *.myDomain.com, 169.254.1.0/24") |
||||
cases := []tc{ |
||||
{"", net.IPv4zero, false}, |
||||
{"", net.IPv6zero, false}, |
||||
|
||||
{"", net.ParseIP("127.0.0.1"), false}, |
||||
{"", net.ParseIP("::1"), false}, |
||||
|
||||
{"", net.ParseIP("10.0.1.1"), true}, |
||||
{"", net.ParseIP("192.168.1.1"), true}, |
||||
{"", net.ParseIP("fd00::1"), true}, |
||||
|
||||
{"", net.ParseIP("8.8.8.8"), true}, |
||||
{"", net.ParseIP("1001::1"), true}, |
||||
|
||||
{"mydomain.com", net.IPv4zero, false}, |
||||
{"sub.mydomain.com", net.IPv4zero, true}, |
||||
|
||||
{"", net.ParseIP("169.254.1.1"), true}, |
||||
{"", net.ParseIP("169.254.2.2"), false}, |
||||
} |
||||
for _, c := range cases { |
||||
assert.Equalf(t, c.expected, hl.MatchesHostOrIP(c.host, c.ip), "case %s(%v)", c.host, c.ip) |
||||
} |
||||
|
||||
hl = ParseHostMatchList("loopback") |
||||
cases = []tc{ |
||||
{"", net.IPv4zero, false}, |
||||
{"", net.ParseIP("127.0.0.1"), true}, |
||||
{"", net.ParseIP("10.0.1.1"), false}, |
||||
{"", net.ParseIP("192.168.1.1"), false}, |
||||
{"", net.ParseIP("8.8.8.8"), false}, |
||||
|
||||
{"", net.ParseIP("::1"), true}, |
||||
{"", net.ParseIP("fd00::1"), false}, |
||||
{"", net.ParseIP("1000::1"), false}, |
||||
|
||||
{"mydomain.com", net.IPv4zero, false}, |
||||
} |
||||
for _, c := range cases { |
||||
assert.Equalf(t, c.expected, hl.MatchesHostOrIP(c.host, c.ip), "case %s(%v)", c.host, c.ip) |
||||
} |
||||
|
||||
hl = ParseHostMatchList("private") |
||||
cases = []tc{ |
||||
{"", net.IPv4zero, false}, |
||||
{"", net.ParseIP("127.0.0.1"), false}, |
||||
{"", net.ParseIP("10.0.1.1"), true}, |
||||
{"", net.ParseIP("192.168.1.1"), true}, |
||||
{"", net.ParseIP("8.8.8.8"), false}, |
||||
|
||||
{"", net.ParseIP("::1"), false}, |
||||
{"", net.ParseIP("fd00::1"), true}, |
||||
{"", net.ParseIP("1000::1"), false}, |
||||
|
||||
{"mydomain.com", net.IPv4zero, false}, |
||||
} |
||||
for _, c := range cases { |
||||
assert.Equalf(t, c.expected, hl.MatchesHostOrIP(c.host, c.ip), "case %s(%v)", c.host, c.ip) |
||||
} |
||||
|
||||
hl = ParseHostMatchList("external") |
||||
cases = []tc{ |
||||
{"", net.IPv4zero, false}, |
||||
{"", net.ParseIP("127.0.0.1"), false}, |
||||
{"", net.ParseIP("10.0.1.1"), false}, |
||||
{"", net.ParseIP("192.168.1.1"), false}, |
||||
{"", net.ParseIP("8.8.8.8"), true}, |
||||
|
||||
{"", net.ParseIP("::1"), false}, |
||||
{"", net.ParseIP("fd00::1"), false}, |
||||
{"", net.ParseIP("1000::1"), true}, |
||||
|
||||
{"mydomain.com", net.IPv4zero, false}, |
||||
} |
||||
for _, c := range cases { |
||||
assert.Equalf(t, c.expected, hl.MatchesHostOrIP(c.host, c.ip), "case %s(%v)", c.host, c.ip) |
||||
} |
||||
|
||||
hl = ParseHostMatchList("*") |
||||
cases = []tc{ |
||||
{"", net.IPv4zero, true}, |
||||
{"", net.ParseIP("127.0.0.1"), true}, |
||||
{"", net.ParseIP("10.0.1.1"), true}, |
||||
{"", net.ParseIP("192.168.1.1"), true}, |
||||
{"", net.ParseIP("8.8.8.8"), true}, |
||||
|
||||
{"", net.ParseIP("::1"), true}, |
||||
{"", net.ParseIP("fd00::1"), true}, |
||||
{"", net.ParseIP("1000::1"), true}, |
||||
|
||||
{"mydomain.com", net.IPv4zero, true}, |
||||
} |
||||
for _, c := range cases { |
||||
assert.Equalf(t, c.expected, hl.MatchesHostOrIP(c.host, c.ip), "case %s(%v)", c.host, c.ip) |
||||
} |
||||
} |
@ -0,0 +1,19 @@ |
||||
// 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 util |
||||
|
||||
import ( |
||||
"net" |
||||
) |
||||
|
||||
// IsIPPrivate for net.IP.IsPrivate. TODO: replace with `ip.IsPrivate()` if min go version is bumped to 1.17
|
||||
func IsIPPrivate(ip net.IP) bool { |
||||
if ip4 := ip.To4(); ip4 != nil { |
||||
return ip4[0] == 10 || |
||||
(ip4[0] == 172 && ip4[1]&0xf0 == 16) || |
||||
(ip4[0] == 192 && ip4[1] == 168) |
||||
} |
||||
return len(ip) == net.IPv6len && ip[0]&0xfe == 0xfc |
||||
} |
Loading…
Reference in new issue