netguard

package
v0.0.0-...-ba2e97a Latest Latest
Warning

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

Go to latest
Published: Sep 1, 2026 License: Apache-2.0 Imports: 6 Imported by: 0

Documentation

Overview

Package netguard provides a shared, dial-time SSRF guard for outbound HTTP(S) calls whose destination is influenced, even partially, by untrusted input (request bodies, tenant configuration, redirects).

The guard resolves the target host and validates every candidate address against a caller-supplied Policy inside the dial itself — not as a separate pre-check — so a DNS answer that changes between a check and a later connection attempt (DNS rebinding) cannot smuggle a disallowed address past the guard. Once a connection has been dialed and validated, reusing it from an http.Transport's connection pool carries no additional rebinding risk: the socket is already bound to the specific IP that was checked, and rebinding only affects a *future* dial for that hostname.

netguard has no built-in opinion on which addresses are legitimate — that is a property of the caller's own threat model (see Policy and the presets in presets.go) — and it has no awareness of HTTP proxying. A dial-time guard wired via DialContext only ever sees, and can only ever validate, the address this process itself dials; when a forward proxy is configured, that address is the proxy's, never the proxied origin's. See the httpclient package for how the two are composed.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func CheckRedirect

func CheckRedirect(policy Policy, maxRedirects int) func(*http.Request, []*http.Request) error

CheckRedirect builds an http.Client.CheckRedirect callback that bounds redirect loops, restricts the scheme of each hop to policy.AllowedSchemes, and refuses any hop that leaves the host of the original request.

The host restriction exists because callers of a guarded client routinely pass their own credentials as headers (an upstream's auth header, for instance); net/http only strips Authorization/Cookie-style headers across a cross-host redirect, and forwards any custom header name verbatim. A malicious or compromised upstream could otherwise answer with a redirect to a host it controls and be handed the caller's credential. Same-host redirects still dial through the guarded DialContext, so the address policy is re-applied on every hop, not just the first.

maxRedirects <= 0 uses defaultMaxRedirects (5); a negative value is not distinguished from zero — pass a Transport/Client with CheckRedirect itself set to reject all redirects if none should ever be followed.

func DialContext

func DialContext(policy Policy, timeout time.Duration) func(context.Context, string, string) (net.Conn, error)

DialContext returns a dial function suitable for http.Transport.DialContext (or direct use with a net.Dialer-shaped caller) that resolves the target host, refuses to dial any candidate address the policy disallows, and then dials the exact resolved address it just approved — never the original hostname string again. Performing the resolution and the connection in a single step is what closes the DNS-rebinding window: a name that resolves to an allowed address during validation cannot resolve to a different one by the time the connection is actually made, because no time passes between the two.

Rejections return a generic error; the specific resolved address and reason are deliberately not included, so a caller returning this error to an end user does not leak internal network topology. Callers that want the concrete reason for internal logging should wrap this dialer and inspect the address themselves before calling it.

func Validate

func Validate(ctx context.Context, policy Policy, host string) error

Validate resolves host and checks every candidate address against policy, without dialing. It exists for callers that must validate a destination locally before routing the actual connection somewhere this package has no visibility into — e.g. httpclient's ProxyEgressManualCONNECT mode, which validates an origin hostname before handing it to a forward proxy in a CONNECT request. This is defense-in-depth only: it reflects what THIS process resolves, not necessarily what a downstream proxy will actually connect to.

Types

type Policy

type Policy struct {
	// BlockPrivate refuses RFC 1918 (IPv4) and unique local (IPv6 ULA,
	// fc00::/7) addresses.
	BlockPrivate bool
	// BlockLoopback refuses 127.0.0.0/8 and ::1.
	BlockLoopback bool
	// BlockLinkLocal refuses 169.254.0.0/16 and fe80::/10 — this is where
	// the cloud instance metadata endpoint (169.254.169.254) lives, so this
	// is refused by every preset regardless of the private-address stance.
	BlockLinkLocal bool
	// BlockUnspecified refuses 0.0.0.0 and ::, which the OS can reinterpret
	// as "local host".
	BlockUnspecified bool
	// BlockMulticastBroadcast refuses multicast and the IPv4 broadcast
	// address, neither of which is a meaningful HTTP peer.
	BlockMulticastBroadcast bool
	// BlockCGNAT refuses 100.64.0.0/10 (RFC 6598 carrier-grade NAT shared
	// address space), which net.IP.IsPrivate does not cover but which can
	// still route to internal infrastructure.
	BlockCGNAT bool

	// DenyCIDRs lists additional address ranges to refuse, beyond the
	// Block* categories above (e.g. an operator's own internal VPC CIDR).
	DenyCIDRs []*net.IPNet
	// AllowCIDRs narrows DenyCIDRs and the Block* categories: an address
	// matching AllowCIDRs is permitted even if it would otherwise be
	// refused. This is an explicit, off-by-default admin opt-in — it must
	// never be used to widen policy implicitly, only to carve out a
	// specific, deliberately-approved exception.
	AllowCIDRs []*net.IPNet

	// AllowedSchemes lists the URL schemes CheckRedirect permits a redirect
	// to target. Empty defaults to {"https"} — "http" must be added
	// explicitly.
	AllowedSchemes []string
}

Policy describes which resolved IP addresses a guarded dial may connect to. The zero value rejects every address — a Policy must be built via one of the presets in presets.go, or assembled explicitly, before use; there is deliberately no "default" stance, since two legitimate use cases already in this codebase disagree (one permits private/RFC1918 ranges as ordinary upstreams, the other requires a fully public address).

func PermitPrivateBlockMetadata

func PermitPrivateBlockMetadata() Policy

PermitPrivateBlockMetadata returns the policy appropriate for calls to an operator- or tenant-configured backend that is normally *meant* to be private — a Kubernetes ClusterIP, a service-DNS name resolving into RFC 1918 space, or a localhost port during development. Private, loopback, and carrier-grade-NAT addresses are all permitted so ordinary deployment shapes keep working.

What stays refused is the set of addresses that is never a legitimate upstream but is a standard SSRF target or a hazard: link-local addresses (which is where the cloud instance metadata endpoint 169.254.169.254 lives), the unspecified address (which the OS can reinterpret as "local host"), and multicast/broadcast addresses.

func PublicOnly

func PublicOnly() Policy

PublicOnly returns the stricter policy appropriate for fetching a URL that is expected to point at the public internet (a vendor endpoint, a third-party spec URL) — every private, loopback, link-local, carrier-grade-NAT, unspecified, and multicast/broadcast address is refused.

Jump to

Keyboard shortcuts

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