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  }