mirror of
https://github.com/amir20/dozzle.git
synced 2026-08-07 08:54:45 +00:00
fix(security): block IPv6 transition addresses in webhook SSRF guard (#4887)
This commit is contained in:
@@ -10,6 +10,7 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"slices"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
@@ -30,6 +31,16 @@ var errBlockedAddress = errors.New("webhook target resolves to a blocked address
|
||||
var zeroNetV4 = &net.IPNet{IP: net.IP{0, 0, 0, 0}, Mask: net.CIDRMask(8, 32)}
|
||||
|
||||
func isBlockedIP(ip net.IP) bool {
|
||||
if isBlockedBaseIP(ip) {
|
||||
return true
|
||||
}
|
||||
// IPv6 transition mechanisms (6to4, NAT64, Teredo, IPv4-compatible) embed an
|
||||
// arbitrary IPv4 address that none of the checks above look at. Unwrap and
|
||||
// re-check the embedded address so 2002:7f00:1::1 is treated as 127.0.0.1.
|
||||
return slices.ContainsFunc(embeddedIPv4(ip), isBlockedBaseIP)
|
||||
}
|
||||
|
||||
func isBlockedBaseIP(ip net.IP) bool {
|
||||
if ip.IsLoopback() ||
|
||||
ip.IsLinkLocalUnicast() ||
|
||||
ip.IsLinkLocalMulticast() ||
|
||||
@@ -50,6 +61,59 @@ func isBlockedIP(ip net.IP) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// embeddedIPv4 returns the IPv4 addresses carried inside an IPv6 transition
|
||||
// address, or nil when the address carries none. Teredo yields two: the relay
|
||||
// server and the (obfuscated) client.
|
||||
func embeddedIPv4(ip net.IP) []net.IP {
|
||||
if ip.To4() != nil {
|
||||
return nil
|
||||
}
|
||||
ip16 := ip.To16()
|
||||
if ip16 == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch {
|
||||
// 6to4 — RFC 3056, 2002::/16, IPv4 in bytes 2-6
|
||||
case ip16[0] == 0x20 && ip16[1] == 0x02:
|
||||
return []net.IP{net.IPv4(ip16[2], ip16[3], ip16[4], ip16[5])}
|
||||
|
||||
// NAT64 well-known prefix — RFC 6052, 64:ff9b::/96, IPv4 in the low 32 bits
|
||||
case ip16[0] == 0x00 && ip16[1] == 0x64 && ip16[2] == 0xff && ip16[3] == 0x9b &&
|
||||
isZeros(ip16[4:12]):
|
||||
return []net.IP{net.IPv4(ip16[12], ip16[13], ip16[14], ip16[15])}
|
||||
|
||||
// NAT64 local-use prefix — RFC 8215, 64:ff9b:1::/48. The embedded IPv4 sits
|
||||
// at a position that depends on the operator's prefix length, so block the
|
||||
// whole range rather than guess.
|
||||
case ip16[0] == 0x00 && ip16[1] == 0x64 && ip16[2] == 0xff && ip16[3] == 0x9b && ip16[4] == 0x00 && ip16[5] == 0x01:
|
||||
return []net.IP{net.IPv4zero}
|
||||
|
||||
// Teredo — RFC 4380, 2001::/32. Server IPv4 in bytes 4-8, client IPv4 in
|
||||
// bytes 12-16 obfuscated by XOR with 0xff.
|
||||
case ip16[0] == 0x20 && ip16[1] == 0x01 && ip16[2] == 0x00 && ip16[3] == 0x00:
|
||||
return []net.IP{
|
||||
net.IPv4(ip16[4], ip16[5], ip16[6], ip16[7]),
|
||||
net.IPv4(ip16[12]^0xff, ip16[13]^0xff, ip16[14]^0xff, ip16[15]^0xff),
|
||||
}
|
||||
|
||||
// IPv4-compatible — deprecated ::a.b.c.d, not unwrapped by net.IP.To4
|
||||
case isZeros(ip16[0:12]):
|
||||
return []net.IP{net.IPv4(ip16[12], ip16[13], ip16[14], ip16[15])}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func isZeros(b []byte) bool {
|
||||
for _, x := range b {
|
||||
if x != 0 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func safeDialContext(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
host, port, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
|
||||
@@ -226,9 +226,21 @@ func TestIsBlockedIP(t *testing.T) {
|
||||
"fe80::1",
|
||||
"224.0.0.1",
|
||||
"0.0.0.0",
|
||||
"0.1.2.3", // 0.0.0.0/8 — routes to localhost on Linux
|
||||
"0.255.255.255", // top of 0.0.0.0/8
|
||||
"0.1.2.3", // 0.0.0.0/8 — routes to localhost on Linux
|
||||
"0.255.255.255", // top of 0.0.0.0/8
|
||||
"255.255.255.255", // limited broadcast
|
||||
|
||||
// IPv6 transition addresses embedding a blocked IPv4
|
||||
"2002:7f00:0001::1", // 6to4 -> 127.0.0.1
|
||||
"2002:a9fe:a9fe::1", // 6to4 -> 169.254.169.254
|
||||
"2002:0000:0001::1", // 6to4 -> 0.0.0.1
|
||||
"64:ff9b::7f00:1", // NAT64 WKP -> 127.0.0.1
|
||||
"64:ff9b::a9fe:a9fe", // NAT64 WKP -> 169.254.169.254
|
||||
"64:ff9b:1::7f00:1", // NAT64 local-use prefix
|
||||
"2001:0000:dead:beef:0000:0000:80ff:fffe", // Teredo -> client 127.0.0.1
|
||||
"2001:0000:7f00:0001::1", // Teredo -> server 127.0.0.1
|
||||
"::7f00:1", // IPv4-compatible -> 127.0.0.1
|
||||
"::a9fe:a9fe", // IPv4-compatible -> 169.254.169.254
|
||||
}
|
||||
for _, s := range blocked {
|
||||
ip := net.ParseIP(s)
|
||||
@@ -242,6 +254,9 @@ func TestIsBlockedIP(t *testing.T) {
|
||||
"172.16.5.10",
|
||||
"8.8.8.8",
|
||||
"2606:4700:4700::1111",
|
||||
"2002:0808:0808::1", // 6to4 -> 8.8.8.8
|
||||
"64:ff9b::808:808", // NAT64 WKP -> 8.8.8.8
|
||||
"2001:0:808:808::1", // Teredo with public server/client
|
||||
}
|
||||
for _, s := range allowed {
|
||||
ip := net.ParseIP(s)
|
||||
|
||||
Reference in New Issue
Block a user