safehttp

package
v1.21.9 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 17, 2026 License: Apache-2.0 Imports: 7 Imported by: 0

Documentation

Overview

Package safehttp provides the shared SSRF-validation and dial-time IP-pinning core for server-originated outbound HTTP.

It lives below both api/ and auth/ in the import graph (it imports only the standard library) so that both packages can share one SSRF blocklist and one pinning dialer. auth/ cannot import api/ (api already imports auth), which is why this core is extracted here instead of living in api/.

Two consumers:

  • api/ reuses CheckIP / IsBlockedLocalhostName as the single source of truth for the SSRF IP blocklist used by its URIValidator and SafeHTTPClient.
  • auth/ (OAuth/OIDC provider clients, OIDC discovery, SAML metadata fetch) builds its outbound *http.Client via NewHardenedClient, which pins the dialed IP for every request so an admin-set internal token_url / userinfo / issuer / metadata URL is blocked at dial time.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func CheckIP

func CheckIP(ip net.IP) error

CheckIP returns a non-nil error when ip falls in a range that server-originated outbound HTTP must never reach: the unspecified address (0.0.0.0, ::), loopback, RFC1918 private space, the cloud-metadata endpoint (169.254.169.254), or any other link-local address. It is the single source of truth for the SSRF IP blocklist shared by api/ and auth/. SEM@8ca5b826b2afb7199c5784087940df39d67cbaeb: validate that an IP is not in any SSRF-blocked range (unspecified, loopback, private, link-local, metadata) (pure)

func IsBlockedLocalhostName

func IsBlockedLocalhostName(host string) bool

IsBlockedLocalhostName reports whether host is a textual localhost alias that must be blocked before DNS resolution. These usually resolve to loopback (and would be caught by CheckIP), but blocking by name is defense-in-depth against a resolver that maps them elsewhere. SEM@e55d63794c48585aafab36880122df63ab8ab1be: validate a hostname as a localhost alias that must be blocked before DNS resolution (pure)

func NewHardenedClient

func NewHardenedClient(opts HardenedClientOptions) *http.Client

NewHardenedClient builds an *http.Client whose transport validates and pins the dialed IP for every request (SSRF defense at dial time) and which refuses all redirects. It is the shared egress path for auth/ provider, OIDC discovery, and SAML-metadata outbound HTTP, where the destination URL is an admin-set, runtime-mutable setting.

Proxy support is intentionally disabled: routing through an HTTP proxy would resolve the target host at the proxy and defeat dial-time IP pinning, reopening the SSRF hole this client closes. SEM@e55d63794c48585aafab36880122df63ab8ab1be: build an HTTP client with IP-pinning, SSRF blocklist enforcement, and redirect refusal (pure)

func RefuseRedirects

func RefuseRedirects(req *http.Request, _ []*http.Request) error

RefuseRedirects is an http.Client CheckRedirect policy that blocks every redirect. Provider token/userinfo/issuer and SAML metadata URLs are runtime-mutable settings; following a redirect (Go re-sends the request body on 307/308) would let a hostile or compromised endpoint bounce a secret-bearing request to an internal or attacker-chosen target (SSRF). SEM@e55d63794c48585aafab36880122df63ab8ab1be: reject all HTTP redirects to prevent SSRF via redirect-bounce on provider endpoints (pure)

Types

type HardenedClientOptions

type HardenedClientOptions struct {
	// Timeout is the overall per-request timeout (http.Client.Timeout). A
	// non-positive value leaves the client with no overall timeout (callers
	// should always set one).
	Timeout time.Duration
	// DialTimeout bounds a single dial attempt. Defaults to 10s.
	DialTimeout time.Duration
	// Resolver overrides DNS resolution. Defaults to the system resolver.
	Resolver HostResolver
	// TransportWrap, when non-nil, wraps the pinning transport (e.g.
	// otelhttp.NewTransport) for instrumentation.
	TransportWrap func(http.RoundTripper) http.RoundTripper
	// AllowHost, when non-nil and returning true for a dialed host, bypasses
	// the SSRF blocklist for that host. Intended ONLY for tests that must reach
	// a loopback httptest server; production callers leave it nil.
	AllowHost func(host string) bool
}

HardenedClientOptions configures NewHardenedClient. SEM@e55d63794c48585aafab36880122df63ab8ab1be: configuration options for building an SSRF-resistant outbound HTTP client (pure)

type HostResolver

type HostResolver interface {
	LookupHost(ctx context.Context, host string) ([]string, error)
}

HostResolver looks up the IPs for a hostname. Implementations must be safe for concurrent use. SEM@e55d63794c48585aafab36880122df63ab8ab1be: interface for concurrent-safe hostname-to-IP resolution

var DefaultResolver HostResolver = defaultResolver{}

DefaultResolver is the shared system-DNS HostResolver. Consumers above this package (api/, auth/) reuse it instead of defining their own copy. SEM@e55d63794c48585aafab36880122df63ab8ab1be: shared system-DNS HostResolver value reused by api and auth (pure)

type PinningDialer

type PinningDialer struct {
	// contains filtered or unexported fields
}

PinningDialer is an http.Transport DialContext that validates the dial target against the SSRF blocklist and pins the connection to a validated IP. For a hostname it resolves once, rejects the dial if ANY resolved IP is blocked, and dials the first resolved IP — closing the validate/connect DNS-rebinding window. For a literal IP it validates directly. SEM@e55d63794c48585aafab36880122df63ab8ab1be: SSRF-resistant dialer that validates and pins outbound connections to a blocklist-cleared IP (pure)

func NewPinningDialer

func NewPinningDialer(resolver HostResolver, allowHost func(host string) bool, dialTimeout time.Duration) *PinningDialer

NewPinningDialer builds a PinningDialer. resolver defaults to the system resolver when nil. allowHost, when non-nil and returning true for a dialed host, bypasses the SSRF blocklist for that host; it is intended only for tests that must reach a loopback httptest server and is never set in production. dialTimeout defaults to 10s when non-positive. SEM@f69d82c5864c65f768181d82416962e3bbd63667: build a pinning dialer with configurable resolver, allow-list bypass, and dial timeout (pure)

func (*PinningDialer) DialContext

func (d *PinningDialer) DialContext(ctx context.Context, network, addr string) (net.Conn, error)

DialContext validates network/addr and dials a pinned, blocklist-cleared IP. SEM@e55d63794c48585aafab36880122df63ab8ab1be: validate dial target against SSRF blocklist and connect to a pinned, blocklist-cleared IP

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL