porting all github actions from bluesky-social/indigo to tangled CI

basic IP filtering net.Dialer for SSRF protection (#1057)

Pulls in code from this 2019 blog post which still seems like the best
pattern:
https://www.agwa.name/blog/post/preventing_server_side_request_forgery_in_golang

I looked around a bit and there are some packages that do similar
things, but this seemed the best, and only a few dozen lines of code so
just vendoring it in.

This code has no deps, so I put it in a sub-package of `util`, which
will make it painless to depend on from other code (eg, OAuth client,
relay) without entangling with any new deps.

authored by bnewbold.net and committed by GitHub 41da898e 6ece2ee0

Changed files
+160
util
+128
util/ssrf/ssrf.go
··· 1 + /* 2 + * Written in 2019 by Andrew Ayer. 3 + * Patched 2025, Bluesky Social PBC. 4 + * 5 + * Original: https://www.agwa.name/blog/post/preventing_server_side_request_forgery_in_golang 6 + * 7 + * To the extent possible under law, the author(s) have dedicated all 8 + * copyright and related and neighboring rights to this software to the 9 + * public domain worldwide. This software is distributed without any 10 + * warranty. 11 + * 12 + * You should have received a copy of the CC0 Public 13 + * Domain Dedication along with this software. If not, see 14 + * <https://creativecommons.org/publicdomain/zero/1.0/>. 15 + */ 16 + package ssrf 17 + 18 + import ( 19 + "fmt" 20 + "net" 21 + "net/http" 22 + "syscall" 23 + "time" 24 + ) 25 + 26 + func ipv4Net(a, b, c, d byte, subnetPrefixLen int) net.IPNet { 27 + return net.IPNet{ 28 + IP: net.IPv4(a, b, c, d), 29 + Mask: net.CIDRMask(96+subnetPrefixLen, 128), 30 + } 31 + } 32 + 33 + var reservedIPv4Nets = []net.IPNet{ 34 + ipv4Net(0, 0, 0, 0, 8), // Current network 35 + ipv4Net(10, 0, 0, 0, 8), // Private 36 + ipv4Net(100, 64, 0, 0, 10), // RFC6598 37 + ipv4Net(127, 0, 0, 0, 8), // Loopback 38 + ipv4Net(169, 254, 0, 0, 16), // Link-local 39 + ipv4Net(172, 16, 0, 0, 12), // Private 40 + ipv4Net(192, 0, 0, 0, 24), // RFC6890 41 + ipv4Net(192, 0, 2, 0, 24), // Test, doc, examples 42 + ipv4Net(192, 88, 99, 0, 24), // IPv6 to IPv4 relay 43 + ipv4Net(192, 168, 0, 0, 16), // Private 44 + ipv4Net(198, 18, 0, 0, 15), // Benchmarking tests 45 + ipv4Net(198, 51, 100, 0, 24), // Test, doc, examples 46 + ipv4Net(203, 0, 113, 0, 24), // Test, doc, examples 47 + ipv4Net(224, 0, 0, 0, 4), // Multicast 48 + ipv4Net(240, 0, 0, 0, 4), // Reserved (includes broadcast / 255.255.255.255) 49 + } 50 + 51 + var globalUnicastIPv6Net = net.IPNet{ 52 + IP: net.IP{0x20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 53 + Mask: net.CIDRMask(3, 128), 54 + } 55 + 56 + func isIPv6GlobalUnicast(address net.IP) bool { 57 + return globalUnicastIPv6Net.Contains(address) 58 + } 59 + 60 + func isIPv4Reserved(address net.IP) bool { 61 + for _, reservedNet := range reservedIPv4Nets { 62 + if reservedNet.Contains(address) { 63 + return true 64 + } 65 + } 66 + return false 67 + } 68 + 69 + func IsPublicIPAddress(address net.IP) bool { 70 + if address.To4() != nil { 71 + return !isIPv4Reserved(address) 72 + } else { 73 + return isIPv6GlobalUnicast(address) 74 + } 75 + } 76 + 77 + // 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. 78 + func PublicOnlyControl(network string, address string, conn syscall.RawConn) error { 79 + if !(network == "tcp4" || network == "tcp6") { 80 + return fmt.Errorf("%s is not a safe network type", network) 81 + } 82 + 83 + host, port, err := net.SplitHostPort(address) 84 + if err != nil { 85 + return fmt.Errorf("%s is not a valid host/port pair: %s", address, err) 86 + } 87 + 88 + ipaddress := net.ParseIP(host) 89 + if ipaddress == nil { 90 + return fmt.Errorf("%s is not a valid IP address", host) 91 + } 92 + 93 + if !IsPublicIPAddress(ipaddress) { 94 + return fmt.Errorf("%s is not a public IP address", ipaddress) 95 + } 96 + 97 + if !(port == "80" || port == "443") { 98 + return fmt.Errorf("%s is not a safe port number", port) 99 + } 100 + 101 + return nil 102 + } 103 + 104 + // [net.Dialer] with [PublicOnlyControl] for `Control` function (for SSRF protection). Other fields are same default values as standard library. 105 + func PublicOnlyDialer() *net.Dialer { 106 + return &net.Dialer{ 107 + Timeout: 30 * time.Second, 108 + KeepAlive: 30 * time.Second, 109 + DualStack: true, 110 + Control: PublicOnlyControl, 111 + } 112 + } 113 + 114 + // [http.Transport] with [PublicOnlyDialer] for `DialContext` field (for SSRF protection). Other fields are same default values as standard library. 115 + // 116 + // Use this in an [http.Client] like: `c := http.Client{ Transport: PublicOnlyTransport() }` 117 + func PublicOnlyTransport() *http.Transport { 118 + dialer := PublicOnlyDialer() 119 + return &http.Transport{ 120 + Proxy: http.ProxyFromEnvironment, 121 + DialContext: dialer.DialContext, 122 + ForceAttemptHTTP2: true, 123 + MaxIdleConns: 100, 124 + IdleConnTimeout: 90 * time.Second, 125 + TLSHandshakeTimeout: 10 * time.Second, 126 + ExpectContinueTimeout: 1 * time.Second, 127 + } 128 + }
+32
util/ssrf/ssrf_test.go
··· 1 + package ssrf 2 + 3 + import ( 4 + "net/http" 5 + "testing" 6 + 7 + "github.com/stretchr/testify/assert" 8 + ) 9 + 10 + func TestPublicOnlyTransport(t *testing.T) { 11 + t.Skip("skipping local SSRF test") 12 + assert := assert.New(t) 13 + 14 + c := http.Client{ 15 + Transport: PublicOnlyTransport(), 16 + } 17 + 18 + { 19 + _, err := c.Get("http://127.0.0.1:2470/") 20 + assert.Error(err) 21 + } 22 + 23 + { 24 + _, err := c.Get("http://localhost:2470/path") 25 + assert.Error(err) 26 + } 27 + 28 + { 29 + _, err := c.Get("http://bsky.app:8080/path") 30 + assert.Error(err) 31 + } 32 + }