safeclient

package
v0.104.0 Latest Latest
Warning

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

Go to latest
Published: Aug 20, 2026 License: MIT Imports: 15 Imported by: 0

Documentation

Overview

Package safeclient provides an SSRF-hardened HTTP client for delivering outbound requests to user-supplied endpoints. It is built on github.com/doyensec/safeurl, which validates the resolved IP at connection time (so it is safe against DNS rebinding), and layers on an explicit, auditable denylist plus synchronous pre-checks.

Policy enforced by this package:

  • only scheme https, only port 443;
  • all internal/reserved IP space is blocked, plus any caller-supplied Config.InfraBlockedCIDRs;
  • redirects are never followed (3xx is surfaced to the caller as the result status);
  • the response body is capped at Config.MaxResponseBytes;
  • the overall request deadline is owned by the CALLER via context.Context — this package imposes no overall request timeout.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrBlockedDestination indicates the destination resolved to (or is) a blocked IP,
	// or otherwise violated the destination policy (credentials in URL, invalid host).
	ErrBlockedDestination = errors.New("safeclient: destination blocked by SSRF policy")

	// ErrBadScheme indicates a scheme other than https.
	ErrBadScheme = errors.New("safeclient: scheme not allowed (https only)")

	// ErrBadPort indicates a port other than 443.
	ErrBadPort = errors.New("safeclient: port not allowed (443 only)")

	// ErrResponseTooLarge indicates the response body exceeded Config.MaxResponseBytes.
	ErrResponseTooLarge = errors.New("safeclient: response body exceeded maximum size")
)

Typed errors returned by the client. Callers (e.g. a retry queue or API validation layer) use errors.Is to distinguish policy-blocked failures — which must NOT be retried and should be surfaced to the user — from transient network failures, which are retryable. Any error returned from Deliver that does not match one of these (nor a context error) should be treated as a transient/retryable network failure.

View Source
var DefaultBlockedCIDRs = []string{
	"0.0.0.0/8",
	"10.0.0.0/8",
	"100.64.0.0/10",
	"127.0.0.0/8",
	"169.254.0.0/16",
	"172.16.0.0/12",
	"192.0.0.0/24",
	"192.0.2.0/24",
	"192.88.99.0/24",
	"192.168.0.0/16",
	"198.18.0.0/15",
	"198.51.100.0/24",
	"203.0.113.0/24",
	"224.0.0.0/4",
	"240.0.0.0/4",
	"255.255.255.255/32",
	"::/128",
	"::1/128",
	"64:ff9b::/96",
	"fc00::/7",
	"fe80::/10",
	"ff00::/8",
}

DefaultBlockedCIDRs is the always-applied denylist of internal/reserved IP space. It is declared explicitly (rather than relying on safeurl's library defaults) so the SSRF policy is auditable in one place and cannot drift with library internals.

NOTE: this list intentionally contains ONLY well-known reserved/private/documentation ranges. Real infrastructure CIDRs (VPC, EKS pod/service ranges, internal load balancers, etc.) must never be hardcoded here — they are supplied at runtime via Config.InfraBlockedCIDRs. The cloud metadata endpoint (169.254.169.254) is already covered by the link-local 169.254.0.0/16 range below.

Functions

func ValidateEndpoint

func ValidateEndpoint(rawURL string) error

ValidateEndpoint runs the synchronous, network-free scheme/port/userinfo checks against a raw URL. It is exposed for reuse at the endpoint-registration API boundary so invalid URLs can be rejected with a clear message at submission time.

This is a UX convenience only: the dial-time IP check in Deliver remains the real enforcement point. Do NOT pre-resolve DNS and store the resolved IP — resolution must happen fresh on every delivery attempt.

Types

type Config

type Config struct {
	InfraBlockedCIDRs []string

	ConnectTimeout       time.Duration
	MaxResponseBytes     int64
	MaxRedirects         int
	AllowEmptyInfraCIDRs bool
	EnableIPv6           bool
	// contains filtered or unexported fields
}

Config controls the SSRF policy and resource limits of a Sender.

type DeliveryResult

type DeliveryResult struct {
	BodyPrefix []byte
	StatusCode int
	Duration   time.Duration
}

DeliveryResult is the outcome of a successful (network-completed) delivery attempt. A 3xx status is reported here, not followed.

type Sender

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

Sender delivers outbound HTTP requests under the SSRF policy. Construct one with New and reuse it; it is safe for concurrent use.

func New

func New(cfg Config, l *zerolog.Logger) (*Sender, error)

New validates cfg, builds the safeurl-backed client, and returns a Sender. It fails if InfraBlockedCIDRs is empty (unless AllowEmptyInfraCIDRs) or if any blocked CIDR — default or infra — fails to parse. l may be nil (logging is then disabled).

func (*Sender) Deliver

func (s *Sender) Deliver(ctx context.Context, method, endpoint string, body []byte, headers http.Header) (*DeliveryResult, error)

Deliver issues a request with the given method, body, and headers to endpoint, enforcing the full SSRF policy. The HTTP method is chosen by the caller (e.g. http.MethodPost, http.MethodGet). It never follows redirects and caps the response body at the configured maximum.

The overall timeout/deadline is owned by the CALLER via ctx: this method sets no http.Client.Timeout and wraps no internal context. If ctx has no deadline, the request may run unbounded — callers should set a deadline.

Retries: callers must re-invoke Deliver for each attempt so that full validation — including fresh DNS resolution and the dial-time IP check — runs every time. Never cache a resolved IP across attempts.

It returns a *DeliveryResult on a completed request (including 3xx), or a typed error: ErrBadScheme, ErrBadPort, ErrBlockedDestination (do not retry — surface to the user), ErrResponseTooLarge, or a wrapped context/network error (retryable). Use errors.Is to distinguish them.

Jump to

Keyboard shortcuts

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