Help us shape the future of Go! Take the 2026 Go Developer Survey

safefetch

package
v0.2.55 Latest Latest
Warning

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

Go to latest
Published: Oct 31, 2026 License: GPL-3.0 Imports: 14 Imported by: 0

Documentation

Overview

Package safefetch guards HTTP fetches of untrusted URLs against SSRF.

URLs that arrive in remote data (an indexer's feed, a definitions listing) can point at cloud metadata endpoints, loopback services or LAN hosts that the remote peer has no business reaching through Ether. A Guard client therefore binds every request to the destination it validated, before any transport sees it:

  • O destino é resolvido e validado UMA vez por RoundTrip (cada hop de redirect é um novo RoundTrip e decide de novo): todo endereço candidato precisa ser público — qualquer um não-público recusa a requisição — e os endereços validados são preservados para o dial direto.
  • Direct connections use a pool key containing the original hostname and validated address set. The dial hook connects only to that set, and TLS uses the original hostname. Proxy requests carry the IP in the absolute URI or CONNECT target; their Host header and TLS name remain original. HTTP proxy requests and HTTPS CONNECT tunnels use dedicated connections.

The one exception is the owner-configured origin: an indexer or a definitions source the owner deliberately pointed at a LAN host must keep working. Callers mark that origin with WithTrustedOrigin; dials to exactly that host:port are allowed whatever address it resolves to. Any other destination — including every redirect hop away from the trusted origin — must be public. Redirects are revalidated on each hop both by CheckRedirect (scheme/length) and by the dialer (address).

Proxy handling is explicit, never a silent bypass of the owner's network setup:

  • A rota é selecionada UMA vez por requisição: a função Proxy do transporte base (configurada ou do ambiente) é chamada uma única vez, a decisão (proxy ou direto, IP validado, hostname original) é guardada no contexto do clone da requisição, e a Proxy func do transporte interno apenas CONSOME essa decisão — nunca chama o seletor de novo. Um seletor stateful não pode mudar a rota entre a validação e o uso.
  • Sem proxy: o dial é direto, no IP já validado (pinado).
  • Com proxy: a rota do dono é PRESERVADA, mas o destino já vai reescrito para o IP validado (acima), e destino não-público é recusado antes de o proxy ser chamado. O host do proxy em si pode ser privado: é a origem de rede que o dono escolheu.
  • Modo não amarrável não é bypass silencioso: proxy com scheme diferente de http (https, socks5, …) ou sem host retorna ErrUnsupportedTransport. Só proxy HTTP (CONNECT / forma absoluta) é suportado por este desenho.

Two further invariants keep the check from being bypassed:

  • Transporte clonável apenas. Só *http.Transport pode ser guardado: ele é clonado para que dial, proxy e pools sejam reconfigurados com enforcement. Qualquer outro RoundTripper (custom/wrapped) produz ErrUnsupportedTransport em cada requisição, em vez de buscar sem pinagem/validação no dial.
  • Separate connection pools per policy. A guarded client carries two transports with independent pools: one only ever dials the exact trusted origin, the other only ever dials public addresses. Each request (and each redirect hop) is routed to one pool by its own destination, so a connection opened to a private trusted origin can never be reused by a later untrusted request to the same host — reuse would skip the dial (and its check) entirely. As variantes direta e via-proxy de cada política também têm pools próprios (exceto os modos por-requisição declarados acima).

Index

Constants

This section is empty.

Variables

View Source
var ErrBlocked = errors.New("safefetch: blocked non-public destination")

ErrBlocked reports a destination the guard refused: a non-http(s) scheme, or a host that resolves to a non-public address.

View Source
var ErrUnsupportedTransport = errors.New("safefetch: unsupported transport")

ErrUnsupportedTransport reports a base transport the guard cannot enforce its policy on: anything that is not a clonable *http.Transport. Guarded clients return it instead of fetching through a custom RoundTripper without dial-time enforcement.

Functions

func IsPublicIP

func IsPublicIP(ip netip.Addr) bool

IsPublicIP reports whether ip is a routable public unicast address. Loopback, RFC 1918 / RFC 4193 private, link-local (including 169.254.0.0/16 cloud metadata), CGNAT, multicast and unspecified addresses are not public.

func SameOrigin

func SameOrigin(a, b string) bool

SameOrigin reports whether two URLs share exactly the same host:port (with default ports filled in). It is how callers decide whether a fetch targets the owner-configured origin at all.

func ValidatePublicURL

func ValidatePublicURL(ctx context.Context, rawURL string) error

ValidatePublicURL validates rawURL with a zero Guard.

func WithTrustedOrigin

func WithTrustedOrigin(ctx context.Context, rawURL string) context.Context

WithTrustedOrigin marks rawURL's origin (host:port, with the scheme's default port filled in) as owner-configured and therefore allowed to resolve to private addresses. Anything else fetched with the returned context must still be public.

func WithTrustedOriginFor

func WithTrustedOriginFor(ctx context.Context, trusted, rawURL string) context.Context

WithTrustedOriginFor marks trusted as the trusted origin only when rawURL actually targets that exact origin; any other rawURL gets an explicitly untrusted (public-only) context. Callers select the policy per fetch with this instead of marking every request trusted and relying on the dialer to sort it out: a request for a feed-supplied URL on another host is public from the start, and a trusted client can never be aimed at a different host.

Types

type Guard

type Guard struct {
	// LookupIP resolves host to addresses; nil uses the system resolver.
	LookupIP func(ctx context.Context, host string) ([]netip.Addr, error)
	// Dial opens a connection to a validated "IP:port" address; nil uses
	// a zero net.Dialer.
	Dial func(ctx context.Context, network, addr string) (net.Conn, error)
}

Guard builds guarded HTTP clients. The zero value is ready to use; LookupIP and Dial exist so tests can map fake public host names onto local test servers.

func (*Guard) Client

func (g *Guard) Client(base *http.Client) *http.Client

Client returns a copy of base whose transport and redirect policy are guarded. The caller selects the policy per request: the owner-configured origin via WithTrustedOrigin/WithTrustedOriginFor on the request context (trusted pool), anything else without it (public pool).

func (*Guard) DialContext

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

DialContext resolves the host in addr, refuses non-public addresses (unless addr is exactly the context's trusted origin) and dials a validated address. Because the check happens here, it applies to the initial request and to every redirect hop alike.

func (*Guard) Transport

func (g *Guard) Transport(base http.RoundTripper) http.RoundTripper

Transport returns a RoundTripper that resolves and validates each destination once per request and binds connections to the validated IP, with separate trusted-origin and public connection pools (see the package documentation). A nil base means http.DefaultTransport. A base that is not a *http.Transport yields a RoundTripper whose requests all fail with ErrUnsupportedTransport, as does a non-http proxy route.

func (*Guard) ValidatePublicURL

func (g *Guard) ValidatePublicURL(ctx context.Context, rawURL string) error

ValidatePublicURL checks that rawURL is an http(s) URL whose host resolves only to public addresses. It validates at call time; fetches should still go through a Guard client, which revalidates at dial time.

Jump to

Keyboard shortcuts

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