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 ¶
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.
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 ¶
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 ¶
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 ¶
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.