safehttp API

safehttp

package

API reference for the safehttp package.

F
function

NewClient

NewClient returns an HTTP client restricted to public network addresses.

Returns

internal/safehttp/safehttp.go:43-59
func NewClient() *http.Client

{
	dialer := &net.Dialer{Timeout: 10 * time.Second, KeepAlive: 30 * time.Second}
	transport := http.DefaultTransport.(*http.Transport).Clone()
	transport.Proxy = nil
	transport.DialContext = publicDialContext(dialer, net.DefaultResolver)

	return &http.Client{
		Transport: transport,
		Timeout:   requestTimeout,
		CheckRedirect: func(req *http.Request, via []*http.Request) error {
			if len(via) >= 10 {
				return fmt.Errorf("stopped after %d redirects", len(via))
			}
			return ValidateURL(req.URL)
		},
	}
}
F
function

NewRequest

NewRequest creates an HTTP request after checking its URL policy.

Parameters

method
string
rawURL
string

Returns

error
internal/safehttp/safehttp.go:62-71
func NewRequest(ctx context.Context, method, rawURL string) (*http.Request, error)

{
	parsed, err := url.ParseRequestURI(rawURL)
	if err != nil {
		return nil, fmt.Errorf("invalid URL %q: %w", rawURL, err)
	}
	if err := ValidateURL(parsed); err != nil {
		return nil, err
	}
	return http.NewRequestWithContext(ctx, method, parsed.String(), nil)
}
F
function

ValidateURL

ValidateURL accepts absolute HTTP and HTTPS URLs without embedded
credentials.

Parameters

parsed

Returns

error
internal/safehttp/safehttp.go:75-86
func ValidateURL(parsed *url.URL) error

{
	if parsed == nil || !parsed.IsAbs() || parsed.Host == "" || parsed.Hostname() == "" {
		return fmt.Errorf("URL must be absolute")
	}
	if parsed.Scheme != "http" && parsed.Scheme != "https" {
		return fmt.Errorf("unsupported URL scheme %q", parsed.Scheme)
	}
	if parsed.User != nil {
		return fmt.Errorf("URL credentials are not allowed")
	}
	return nil
}
F
function

publicDialContext

Parameters

dialer
resolver

Returns

func(context.Context,
string, string) (net.Conn, error)
internal/safehttp/safehttp.go:88-116
func publicDialContext(dialer *net.Dialer, resolver *net.Resolver) func(context.Context, string, string) (net.Conn, error)

{
	return func(ctx context.Context, network, address string) (net.Conn, error) {
		host, port, err := net.SplitHostPort(address)
		if err != nil {
			return nil, fmt.Errorf("invalid network address %q: %w", address, err)
		}

		resolved, err := resolver.LookupNetIP(ctx, "ip", host)
		if err != nil {
			return nil, fmt.Errorf("resolve %q: %w", host, err)
		}
		if len(resolved) == 0 {
			return nil, fmt.Errorf("resolve %q: no addresses", host)
		}

		var lastErr error
		for _, addressIP := range resolved {
			if !isPublic(addressIP) {
				return nil, fmt.Errorf("network address %q is not public", addressIP)
			}
			conn, dialErr := dialer.DialContext(ctx, network, net.JoinHostPort(addressIP.String(), port))
			if dialErr == nil {
				return conn, nil
			}
			lastErr = dialErr
		}
		return nil, fmt.Errorf("connect to %q: %w", host, lastErr)
	}
}
F
function

isPublic

Parameters

address

Returns

bool
internal/safehttp/safehttp.go:118-129
func isPublic(address netip.Addr) bool

{
	if !address.IsValid() {
		return false
	}
	address = address.Unmap()
	for _, network := range blockedNetworks {
		if network.Contains(address) {
			return false
		}
	}
	return address.IsGlobalUnicast()
}
F
function

TestIsPublic

Parameters

internal/safehttp/safehttp_test.go:14-32
func TestIsPublic(t *testing.T)

{
	tests := map[string]bool{
		"8.8.8.8":              true,
		"2606:4700:4700::1111": true,
		"127.0.0.1":            false,
		"10.0.0.1":             false,
		"100.64.0.1":           false,
		"169.254.169.254":      false,
		"192.168.1.1":          false,
		"::1":                  false,
		"fd00::1":              false,
		"fe80::1":              false,
	}
	for rawAddress, want := range tests {
		if got := isPublic(netip.MustParseAddr(rawAddress)); got != want {
			t.Errorf("isPublic(%q) = %t, want %t", rawAddress, got, want)
		}
	}
}
F
function

TestNewRequestRejectsUnsafeURLs

Parameters

internal/safehttp/safehttp_test.go:34-44
func TestNewRequestRejectsUnsafeURLs(t *testing.T)

{
	for _, rawURL := range []string{
		"/relative",
		"file:///tmp/secret",
		"http://user:[email protected]",
	} {
		if _, err := NewRequest(context.Background(), "GET", rawURL); err == nil {
			t.Errorf("expected %q to be rejected", rawURL)
		}
	}
}
F
function

TestPublicDialRejectsLoopback

Parameters

internal/safehttp/safehttp_test.go:46-57
func TestPublicDialRejectsLoopback(t *testing.T)

{
	dial := publicDialContext(
		&net.Dialer{Timeout: time.Second},
		net.DefaultResolver,
	)
	if conn, err := dial(context.Background(), "tcp", "127.0.0.1:80"); err == nil {
		_ = conn.Close()
		t.Fatal("expected loopback address to be rejected")
	} else if !strings.Contains(err.Error(), "not public") {
		t.Fatalf("unexpected rejection error: %v", err)
	}
}