Documentation
¶
Overview ¶
Package httputil provides the hardened egress HTTP client for every fetch of a URL a platform user can inject (schema ingestion today; social login and tenant webhooks later). Endpoints only the operator configures (telemetry collectors, the audit export sink) stay on standard-library clients: the guard exists against server-side request forgery, which requires a user-controlled URL.
The package deliberately imports nothing repo-specific so it can be promoted to a shared library by copying (see the egress-policy ADR).
Index ¶
Constants ¶
This section is empty.
Variables ¶
var ( // ErrResponseTooLarge is returned when the response body exceeds the configured limit. ErrResponseTooLarge = errors.New("response body exceeded maximum allowed size") // ErrTooManyRedirects is returned when the number of redirects exceeds the configured limit. ErrTooManyRedirects = errors.New("stopped after too many redirects") // ErrHTTPSDowngrade is returned when a redirect attempts to downgrade from https to http. ErrHTTPSDowngrade = errors.New("redirect downgrade from https to http is not allowed") // ErrCrossOriginRedirectWithBody is returned when following a redirect // would replay the request body on another origin (307/308). ErrCrossOriginRedirectWithBody = errors.New("refusing to replay a request body across origins on redirect") )
var DefaultDenyList = []string{
"localhost",
"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.168.0.0/16",
"198.18.0.0/15",
"::/128",
"::1/128",
"fc00::/7",
"fe80::/10",
"64:ff9b:1::/48",
"2002::/16",
"2001::/32",
}
DefaultDenyList blocks loopback, private, link-local (cloud metadata), carrier-grade NAT, benchmark, and unspecified ranges, IPv4 and IPv6, plus the "localhost" hostname and the deprecated IPv4-embedding transition prefixes (6to4, Teredo, local-use NAT64). A superset of zitadel/zitadel's HTTPClient.DenyList (GHSA-29jh-8cfq-rr8x). The well-known NAT64 prefix 64:ff9b::/96 is deliberately absent: on DNS64/NAT64 networks every public IPv4 destination appears inside it, so instead the checker evaluates the embedded IPv4 address against these rules (see expandEmbeddedIPv4). The hostname entry is belt-and-braces for the URL layer; the CIDRs are what block at dial time, where every name is already resolved — do not remove one because the other looks equivalent.
Functions ¶
Types ¶
type AddressDeniedError ¶
type AddressDeniedError struct {
// contains filtered or unexported fields
}
AddressDeniedError reports that an egress target was blocked by the deny list, naming the entry that matched so the failure is attributable in logs and to the caller who chose the URL.
func NewAddressDeniedError ¶
func NewAddressDeniedError(deniedBy string) *AddressDeniedError
func (*AddressDeniedError) Error ¶
func (e *AddressDeniedError) Error() string
type ClientConfig ¶
type ClientConfig struct {
// MaxBodySize caps each response body in bytes; 0 means unlimited.
MaxBodySize int64 `mapstructure:"max_body_size"`
// Timeout bounds each single request; 0 means unlimited.
Timeout time.Duration `mapstructure:"timeout"`
// MaxRedirects caps redirect hops; 0 refuses all redirects.
MaxRedirects int `mapstructure:"max_redirects"`
// AllowHTTPSDowngrade permits an https request to redirect to http.
AllowHTTPSDowngrade bool `mapstructure:"allow_https_downgrade"`
// DenyList entries (CIDR, IP, or hostname) block matching targets.
DenyList []string `mapstructure:"deny_list"`
// AllowList entries re-allow targets the deny list blocks, e.g.
// "localhost" plus "127.0.0.0/8" for local development.
AllowList []string `mapstructure:"allow_list"`
}
ClientConfig builds a hardened egress client. The zero value enforces nothing; defaults are the config loader's job.
func (ClientConfig) NewClient ¶
func (c ClientConfig) NewClient() (*http.Client, error)
NewClient returns an *http.Client protected against DNS rebinding (the dial-time policy check on the resolved IP), hostname-denied targets (a pre-connection check on every request, redirect hops included), redirect abuse (hop cap, downgrade block), and oversized responses.
func (ClientConfig) Validate ¶
func (c ClientConfig) Validate() error
Validate parses both lists so a malformed entry fails at startup, not at first fetch, and rejects negative limits: a negative max_body_size would silently disable the response cap.
type ContentTypeError ¶
type ContentTypeError struct {
// contains filtered or unexported fields
}
func (*ContentTypeError) Error ¶
func (e *ContentTypeError) Error() string
type HostChecker ¶
HostChecker matches one policy entry: a CIDR network, a single IP, or a hostname (exact, case-insensitive — no wildcards or subdomain matching).
func NewHostChecker ¶
func NewHostChecker(entry string) (*HostChecker, error)
NewHostChecker classifies entry by parse: CIDR, then IP, then hostname. An empty entry returns nil. An entry containing "/" that is not a valid CIDR is an error rather than a hostname: hostnames cannot contain "/", and the silent alternative would be a deny rule that never matches.
type Policy ¶
type Policy struct {
// contains filtered or unexported fields
}
Policy is the egress decision for one client: a target is allowed when it matches the allow list, denied when it matches the deny list, and allowed otherwise. The allow list is an exception list carved out of the deny list, not a lockdown mode.
func NewPolicy ¶
NewPolicy parses both lists, failing fast on malformed entries so a typo'd CIDR cannot silently weaken the deny list.
func (*Policy) Check ¶
Check applies allow-then-deny to the candidate IPs and address string. At dial time ips holds exactly the IP about to be connected, so an allow entry never lets a sibling DNS answer through: each dial is checked alone.
func (*Policy) CheckAddress ¶
CheckAddress applies the policy to a request's URL hostname before any connection is opened: hostname entries match by name, and a literal-IP host also matches IP and CIDR entries. Domains are deliberately not resolved here — the dial-time check in the transport owns resolved addresses, so a DNS answer cannot bypass anything by being checked twice. Consequence: a hostname allow entry only counters a hostname deny entry; a denial by IP range needs an IP or CIDR allow entry.
type StatusError ¶
func (*StatusError) Error ¶
func (e *StatusError) Error() string