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