Ctrl K
ComunidadGuías…

pkg/safehttp/client.go

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// Package safehttp can be used for making http requests when the hostname is
// not trusted (user inputs). It will avoid SSRF by ensuring that the IP
// address which will connect is not a private address, or loopback. It also
// checks that the port is 80 or 443, not anything else.
//
// Operators can configure trusted private networks (CIDRs) to allow safehttp
// callers to reach private addresses in closed-network deployments.
package safehttp

import (
	"fmt"
	"net"
	"net/http"
	"net/netip"
	"sync/atomic"
	"syscall"
	"time"

	build "github.com/cozy/cozy-stack/pkg/config"
)

// trustedPrivateNetworks holds the parsed CIDRs that are allowed to bypass
// private-IP and port restrictions. Set once at startup via
// SetTrustedPrivateNetworks. Stored as an atomic.Value to avoid data races
// between config/test setup and concurrent outbound requests.
var trustedPrivateNetworks atomic.Value // holds []*net.IPNet

var nat64Prefix = netip.MustParsePrefix("64:ff9b::/96")

// These transition mechanisms cannot be checked as ordinary destination IPs.
var unsupportedTransitionPrefixes = []netip.Prefix{
	netip.MustParsePrefix("64:ff9b:1::/48"), // Network-specific NAT64 layouts
	netip.MustParsePrefix("2002::/16"),      // 6to4
	netip.MustParsePrefix("2001::/32"),      // Teredo
}

// SetTrustedPrivateNetworks parses the given CIDR strings and stores them for
// use by safeControl. It should be called once at startup. If any CIDR is
// invalid, it returns an error and does not update the allowlist.
func SetTrustedPrivateNetworks(cidrs []string) error {
	nets := make([]*net.IPNet, 0, len(cidrs))
	for _, cidr := range cidrs {
		_, ipnet, err := net.ParseCIDR(cidr)
		if err != nil {
			return fmt.Errorf("invalid trusted private network CIDR %q: %w", cidr, err)
		}
		nets = append(nets, ipnet)
	}
	trustedPrivateNetworks.Store(nets)
	return nil
}

func loadTrustedNetworks() []*net.IPNet {
	nets, _ := trustedPrivateNetworks.Load().([]*net.IPNet)
	return nets
}

var safeDialer = &net.Dialer{
	Timeout:   30 * time.Second,
	KeepAlive: 30 * time.Second,
	DualStack: true,
	Control:   safeControl,
}

var safeTransport = &http.Transport{
	// Default values for http.DefaultClient
	Proxy:                 http.ProxyFromEnvironment,
	DialContext:           safeDialer.DialContext,
	ForceAttemptHTTP2:     true,
	MaxIdleConns:          100,
	IdleConnTimeout:       90 * time.Second,
	TLSHandshakeTimeout:   10 * time.Second,
	ExpectContinueTimeout: 1 * time.Second,

	// As we are connecting to a new host each time, it is better to disable
	// keep-alive
	DisableKeepAlives: true,
}

// DefaultClient is an http client that can be used instead of
// http.DefaultClient to avoid SSRF. It has the same default configuration,
// except it disabled keep-alive, as it is probably not useful in such cases.
var DefaultClient = &http.Client{
	Timeout:   10 * time.Second,
	Transport: safeTransport,
}

var transportWithKeepAlive = &http.Transport{
	// Default values for http.DefaultClient
	Proxy:                 http.ProxyFromEnvironment,
	DialContext:           safeDialer.DialContext,
	ForceAttemptHTTP2:     true,
	MaxIdleConns:          100,
	IdleConnTimeout:       90 * time.Second,
	TLSHandshakeTimeout:   10 * time.Second,
	ExpectContinueTimeout: 1 * time.Second,
}

// ClientWithKeepAlive is an http client that can be used to avoid SSRF. And it
// has keep-alive (contrary to safehttp.DefaultClient). The typical use case is
// moving a Cozy.
var ClientWithKeepAlive = &http.Client{
	Transport: transportWithKeepAlive,
}

func safeControl(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)
	}

	ipv6, _ := netip.AddrFromSlice(ipaddress)
	for _, prefix := range unsupportedTransitionPrefixes {
		if prefix.Contains(ipv6) {
			return fmt.Errorf("%s is not a safe transition IP address", host)
		}
	}
	if nat64Prefix.Contains(ipv6) {
		// Apply the same policy to the IPv4 destination behind NAT64.
		ipaddress = ipaddress[12:]
	}

	if ipaddress.IsUnspecified() || ipaddress.IsLinkLocalUnicast() || ipaddress.IsLinkLocalMulticast() {
		return fmt.Errorf("%s is not a valid IP address", host)
	}

	// Trusted private networks bypass private-IP, loopback, and port
	// restrictions only for private or loopback destinations. This is for
	// closed-network deployments where internal services run on private
	// addresses and non-standard ports.
	if (ipaddress.IsPrivate() || ipaddress.IsLoopback()) && isInTrustedNetwork(ipaddress) {
		return nil
	}

	if ipaddress.IsPrivate() {
		return fmt.Errorf("%s is not a public IP address", ipaddress)
	}

	// Allow loopback and custom ports for dev only (127.0.0.1 / localhost), as
	// it can be useful for accepting sharings on cozy.localhost:8080 for
	// example.
	if build.IsDevRelease() {
		return nil
	}

	if ipaddress.IsLoopback() {
		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
}

func isInTrustedNetwork(ip net.IP) bool {
	for _, ipnet := range loadTrustedNetworks() {
		if ipnet.Contains(ip) {
			return true
		}
	}
	return false
}