Documentation
¶
Overview ¶
Package safehttp is the security-critical core that gates every outbound connection made by a scan. It validates user input, resolves DNS once, pins the destination IP for the lifetime of the scan, refuses off-host redirects, caps response bodies and rate-limits both clients and targets. See
Index ¶
- Variables
- func AllowRedirect(target *Target, maxHops int) func(*http.Request, []*http.Request) error
- func DefaultLookup(ctx context.Context, host string) ([]netip.Addr, error)
- func NewClient(opts ClientOpts) *http.Client
- func PinnedDialer(target *Target, timeout time.Duration) *net.Dialer
- type ClientOpts
- type InputPolicy
- type Limiter
- type LookupFunc
- type Policy
- type Resolver
- type Target
- type Validated
Constants ¶
This section is empty.
Variables ¶
var ( // ErrInvalidScheme — the input scheme is not in the allow-list. ErrInvalidScheme = errors.New("safehttp: invalid scheme") // ErrInvalidHost — the hostname could not be parsed, is empty, or is // not a valid FQDN. ErrInvalidHost = errors.New("safehttp: invalid host") // ErrIPLiteral — the user supplied a bare IP; only hostnames are accepted. ErrIPLiteral = errors.New("safehttp: ip literal not accepted") // ErrUserInfo — the URL contains a userinfo component. ErrUserInfo = errors.New("safehttp: userinfo not accepted") // ErrCustomPortBlocked — a non-default port was requested while // allow_custom_ports is false. ErrCustomPortBlocked = errors.New("safehttp: custom port blocked") // ErrPrivateTargetBlocked — every resolved IP is in a blocked range. ErrPrivateTargetBlocked = errors.New("safehttp: private or reserved target blocked") // ErrNoAllowedIP — DNS returned no addresses for the host. ErrNoAllowedIP = errors.New("safehttp: no resolvable address for target") // ErrIPPinViolation — a Dial was attempted against an address other // than the pinned target IP. ErrIPPinViolation = errors.New("safehttp: ip-pin violation") // ErrOffHostRedirect — a redirect pointed away from the original host. ErrOffHostRedirect = errors.New("safehttp: off-host redirect refused") // ErrTooManyRedirects — redirect chain exceeded the configured limit. ErrTooManyRedirects = errors.New("safehttp: too many redirects") // ErrBodyTooLarge — the response body exceeded the configured cap. ErrBodyTooLarge = errors.New("safehttp: response body exceeds cap") )
Sentinel errors. All are matchable via errors.Is.
Functions ¶
func AllowRedirect ¶
AllowRedirect is the http.Client CheckRedirect predicate enforced by NewClient. It refuses any redirect that leaves target.Host (off-host) and caps the redirect chain at maxHops. If maxHops is zero or negative, no redirect is followed at all.
func DefaultLookup ¶
DefaultLookup uses the Go default resolver.
func NewClient ¶
func NewClient(opts ClientOpts) *http.Client
NewClient builds an *http.Client wired to the safehttp guarantees:
- the underlying TCP dial always targets the pinned IP:port;
- SNI and the Host header use the original FQDN;
- redirects are refused when they leave the original host;
- response bodies are wrapped in a length-capped reader.
func PinnedDialer ¶
PinnedDialer returns a *net.Dialer whose Control callback refuses to connect to any address other than target.AddrPort(). It is the second layer of the SSRF defence: callers should pass the pinned address to DialContext directly, and Control is the belt-and-braces check that the address was not silently swapped.
Types ¶
type ClientOpts ¶
type ClientOpts struct {
Target *Target
FollowRedirects bool
MaxRedirects int
MaxBodyBytes int64 // 0 disables the cap
Timeout time.Duration
DialTimeout time.Duration
// TLSConfig overrides the default. Default behaviour is
// InsecureSkipVerify=true with SNI set to Target.Host — appropriate for
// the header and custom-check probes, which must succeed even against
// servers with bad certificates (the TLS probe grades those separately).
TLSConfig *tls.Config
}
ClientOpts configures NewClient. Zero values are filled with conservative defaults — the only required field is Target.
type InputPolicy ¶
type InputPolicy struct {
AllowedSchemes []string
AllowCustomPorts bool
DefaultPort int // typically 443
}
InputPolicy captures the runtime knobs applied during validation.
type Limiter ¶
type Limiter struct {
// contains filtered or unexported fields
}
Limiter is a per-key token-bucket limiter. Buckets are materialised on the first call for a given key and live for the lifetime of the process — acceptable in v1 (a future janitor can evict idle buckets if it becomes a memory pressure point).
func NewLimiter ¶
NewLimiter returns a Limiter that allows up to count events per period per distinct key. A zero or negative count yields a permissive limiter that always allows.
type LookupFunc ¶
LookupFunc resolves a host to a list of IP addresses. The default implementation delegates to net.DefaultResolver.LookupNetIP, but tests (and future caching layers) can inject a custom function.
type Policy ¶
type Policy struct {
// AllowPrivate, when true, lifts the block on RFC1918 + extra reserved
// ranges. Loopback, link-local, multicast and unspecified remain blocked
// because connecting to them never makes sense for a remote scanner.
AllowPrivate bool
// Extra is the operator-supplied prefix list. These are always blocked,
// even when AllowPrivate is true — they express "things I never want
// to scan from this instance".
Extra []netip.Prefix
}
Policy is the runtime decision table for accepting or rejecting a target IP.
type Resolver ¶
type Resolver struct {
Lookup LookupFunc // nil → DefaultLookup
Policy Policy
}
Resolver owns the single-lookup contract: it picks the first IP returned by the resolver that satisfies the policy, and locks the result for the rest of the scan.
type Target ¶
type Target struct {
Scheme string
Host string // FQDN, lowercase, ASCII
Port int
IP netip.Addr
// contains filtered or unexported fields
}
Target is the resolved, validated, IP-pinned destination of a scan. Construction goes exclusively through Resolver.Resolve.
func NewTarget ¶
NewTarget builds a Target directly from already-resolved inputs. It bypasses DNS lookup and the IP policy check — callers must have verified the IP against Policy.IsBlocked themselves (or be writing tests).
The returned Target carries the same pinning guarantees as one produced by Resolver.Resolve: all subsequent PinnedDialer connections must hit ip.
type Validated ¶
Validated is the canonical, accepted form of a user-supplied target.
func ValidateInput ¶
func ValidateInput(raw string, p InputPolicy) (*Validated, error)
ValidateInput parses raw, applies, and returns the canonical components — or a typed error from this package.