github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/pkg/safehttp/client.go (about) 1 // Package safehttp can be used for making http requests when the hostname is 2 // not trusted (user inputs). It will avoid SSRF by ensuring that the IP 3 // address which will connect is not a private address, or loopback. It also 4 // checks that the port is 80 or 443, not anything else. 5 package safehttp 6 7 import ( 8 "fmt" 9 "net" 10 "net/http" 11 "syscall" 12 "time" 13 14 build "github.com/cozy/cozy-stack/pkg/config" 15 ) 16 17 var safeDialer = &net.Dialer{ 18 Timeout: 30 * time.Second, 19 KeepAlive: 30 * time.Second, 20 DualStack: true, 21 Control: safeControl, 22 } 23 24 var safeTransport = &http.Transport{ 25 // Default values for http.DefaultClient 26 Proxy: http.ProxyFromEnvironment, 27 DialContext: safeDialer.DialContext, 28 ForceAttemptHTTP2: true, 29 MaxIdleConns: 100, 30 IdleConnTimeout: 90 * time.Second, 31 TLSHandshakeTimeout: 10 * time.Second, 32 ExpectContinueTimeout: 1 * time.Second, 33 34 // As we are connecting to a new host each time, it is better to disable 35 // keep-alive 36 DisableKeepAlives: true, 37 } 38 39 // DefaultClient is an http client that can be used instead of 40 // http.DefaultClient to avoid SSRF. It has the same default configuration, 41 // except it disabled keep-alive, as it is probably not useful in such cases. 42 var DefaultClient = &http.Client{ 43 Timeout: 10 * time.Second, 44 Transport: safeTransport, 45 } 46 47 var transportWithKeepAlive = &http.Transport{ 48 // Default values for http.DefaultClient 49 Proxy: http.ProxyFromEnvironment, 50 DialContext: safeDialer.DialContext, 51 ForceAttemptHTTP2: true, 52 MaxIdleConns: 100, 53 IdleConnTimeout: 90 * time.Second, 54 TLSHandshakeTimeout: 10 * time.Second, 55 ExpectContinueTimeout: 1 * time.Second, 56 } 57 58 // ClientWithKeepAlive is an http client that can be used to avoid SSRF. And it 59 // has keep-alive (contrary to safehttp.DefaultClient). The typical use case is 60 // moving a Cozy. 61 var ClientWithKeepAlive = &http.Client{ 62 Transport: transportWithKeepAlive, 63 } 64 65 func safeControl(network string, address string, conn syscall.RawConn) error { 66 if !(network == "tcp4" || network == "tcp6") { 67 return fmt.Errorf("%s is not a safe network type", network) 68 } 69 70 host, port, err := net.SplitHostPort(address) 71 if err != nil { 72 return fmt.Errorf("%s is not a valid host/port pair: %s", address, err) 73 } 74 75 ipaddress := net.ParseIP(host) 76 if ipaddress == nil { 77 return fmt.Errorf("%s is not a valid IP address", host) 78 } 79 80 if ipaddress.IsUnspecified() || ipaddress.IsLinkLocalUnicast() || ipaddress.IsLinkLocalMulticast() { 81 return fmt.Errorf("%s is not a valid IP address", host) 82 } 83 84 if ipaddress.IsPrivate() { 85 return fmt.Errorf("%s is not a public IP address", ipaddress) 86 } 87 88 // Allow loopback and custom ports for dev only (127.0.0.1 / localhost), as 89 // it can be useful for accepting sharings on cozy.localhost:8080 for 90 // example. 91 if build.IsDevRelease() { 92 return nil 93 } 94 95 if ipaddress.IsLoopback() { 96 return fmt.Errorf("%s is not a public IP address", ipaddress) 97 } 98 99 if port != "80" && port != "443" { 100 return fmt.Errorf("%s is not a safe port number", port) 101 } 102 103 return nil 104 }