/* * Written in 2019 by Andrew Ayer. * Patched 2025, Bluesky Social PBC. * * Original: https://www.agwa.name/blog/post/preventing_server_side_request_forgery_in_golang * * To the extent possible under law, the author(s) have dedicated all * copyright and related and neighboring rights to this software to the * public domain worldwide. This software is distributed without any * warranty. * * You should have received a copy of the CC0 Public * Domain Dedication along with this software. If not, see * . */ package ssrf import ( "fmt" "net" "net/http" "syscall" "time" ) func ipv4Net(a, b, c, d byte, subnetPrefixLen int) net.IPNet { return net.IPNet{ IP: net.IPv4(a, b, c, d), Mask: net.CIDRMask(96+subnetPrefixLen, 128), } } var reservedIPv4Nets = []net.IPNet{ ipv4Net(0, 0, 0, 0, 8), // Current network ipv4Net(10, 0, 0, 0, 8), // Private ipv4Net(100, 64, 0, 0, 10), // RFC6598 ipv4Net(127, 0, 0, 0, 8), // Loopback ipv4Net(169, 254, 0, 0, 16), // Link-local ipv4Net(172, 16, 0, 0, 12), // Private ipv4Net(192, 0, 0, 0, 24), // RFC6890 ipv4Net(192, 0, 2, 0, 24), // Test, doc, examples ipv4Net(192, 88, 99, 0, 24), // IPv6 to IPv4 relay ipv4Net(192, 168, 0, 0, 16), // Private ipv4Net(198, 18, 0, 0, 15), // Benchmarking tests ipv4Net(198, 51, 100, 0, 24), // Test, doc, examples ipv4Net(203, 0, 113, 0, 24), // Test, doc, examples ipv4Net(224, 0, 0, 0, 4), // Multicast ipv4Net(240, 0, 0, 0, 4), // Reserved (includes broadcast / 255.255.255.255) } var globalUnicastIPv6Net = net.IPNet{ IP: net.IP{0x20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, Mask: net.CIDRMask(3, 128), } func isIPv6GlobalUnicast(address net.IP) bool { return globalUnicastIPv6Net.Contains(address) } func isIPv4Reserved(address net.IP) bool { for _, reservedNet := range reservedIPv4Nets { if reservedNet.Contains(address) { return true } } return false } func IsPublicIPAddress(address net.IP) bool { if address.To4() != nil { return !isIPv4Reserved(address) } else { return isIPv6GlobalUnicast(address) } } // Implementation of [net.Dialer] `Control` field (a function) which avoids some SSRF attacks by rejecting local IPv4 and IPv6 address ranges, and only allowing ports 80 or 443. func PublicOnlyControl(network string, address string, conn syscall.RawConn) error { if !(network == "tcp4" || network == "tcp6") { return fmt.Errorf("%s is not a safe network type", network) } host, port, err := net.SplitHostPort(address) if err != nil { return fmt.Errorf("%s is not a valid host/port pair: %s", address, err) } ipaddress := net.ParseIP(host) if ipaddress == nil { return fmt.Errorf("%s is not a valid IP address", host) } if !IsPublicIPAddress(ipaddress) { return fmt.Errorf("%s is not a public IP address", ipaddress) } if !(port == "80" || port == "443") { return fmt.Errorf("%s is not a safe port number", port) } return nil } // [net.Dialer] with [PublicOnlyControl] for `Control` function (for SSRF protection). Other fields are same default values as standard library. func PublicOnlyDialer() *net.Dialer { return &net.Dialer{ Timeout: 30 * time.Second, KeepAlive: 30 * time.Second, DualStack: true, Control: PublicOnlyControl, } } // [http.Transport] with [PublicOnlyDialer] for `DialContext` field (for SSRF protection). Other fields are same default values as standard library. // // Use this in an [http.Client] like: `c := http.Client{ Transport: PublicOnlyTransport() }` func PublicOnlyTransport() *http.Transport { dialer := PublicOnlyDialer() return &http.Transport{ Proxy: http.ProxyFromEnvironment, DialContext: dialer.DialContext, ForceAttemptHTTP2: true, MaxIdleConns: 100, IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second, } }