httpsec

package
v0.8.0 Latest Latest
Warning

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

Go to latest
Published: Oct 1, 2026 License: GPL-3.0 Imports: 9 Imported by: 0

Documentation

Overview

Package httpsec provides SSRF-safe URL validation and HTTP client construction for the OpenCTEM SDK.

This is a lifted copy of api/pkg/httpsec. Originally the SDK lived in its own Go module and could not import from api/, so the 9+ outbound-client sites across sdk-go/pkg/* each carried a bare &http.Client{Timeout: …} with no dialer-level blocklist. That was fine when the SDK only talked to the pinned OpenCTEM API over a trusted operator-provided URL, but third-party users of the SDK (custom scanners, integration shims) can point it at arbitrary hostnames. Any of those callers inherit the same SSRF gap as the API did before the audit.

This package mirrors api/pkg/httpsec verbatim — when you change one, change the other. A follow-up task tracks hoisting the single canonical copy into a top-level shared Go module; until then, the drift is caught by scripts/security-lint.sh which grep-checks both copies for the same CIDR blocklist.

Any outbound HTTP call in sdk-go/pkg/* that reaches a hostname chosen at runtime (API URL from config, KEV/EPSS feed, bootstrap token endpoint, command poller, lease endpoint) MUST use SafeHTTPClient + ValidateURL, not &http.Client{} directly.

Index

Constants

This section is empty.

Variables

View Source
var AllowLoopback = os.Getenv(allowLoopbackEnvVar) == "1"

AllowLoopback, when set to true, skips the loopback (127.0.0.0/8, ::1/128) branch of the blocklist. All other CIDRs (RFC1918, link-local IMDS, CGNAT, …) remain enforced. Prefer the env var for external test harnesses; this variable is for in-process overrides.

View Source
var SensitiveHeaders = []string{
	"Authorization",
	"Proxy-Authorization",
	"Cookie",
	"X-API-Key",
	"X-Api-Token",
	"X-Auth-Token",
	"X-Agent-Key",
	"X-Agent-API-Key",
	"X-Sensor-API-Key",
	"X-ApiKeys",
	"X-Bootstrap-Token",
}

SensitiveHeaders are request headers that carry credentials. They are stripped from a redirected request whenever the redirect changes origin (scheme, host or port). net/http only strips Authorization/Cookie, and only when the host leaves the original domain — it keeps them on an https->http same-host hop and never touches custom API-key headers.

Functions

func AllowPrivate

func AllowPrivate() bool

AllowPrivate reports the current runtime posture for startup-logging; do not consult it per-call (that happens inside IsIPBlocked).

func CheckAPIBaseURL added in v0.7.0

func CheckAPIBaseURL(raw string) (warning string, err error)

CheckAPIBaseURL validates an operator-supplied OpenCTEM API base URL.

It returns an error for anything that cannot be a sane API endpoint: a non-http(s) scheme, a missing host, or embedded userinfo (credentials in the URL end up in logs and error strings). For a plain-http URL whose host is not loopback it returns a non-empty warning instead of an error: the API key is sent as a bearer token, so clear-text transport outside a local dev setup exposes it, but rejecting it outright would break existing in-cluster deployments that terminate TLS elsewhere.

func FirstWarning added in v0.8.0

func FirstWarning(baseURL string) bool

FirstWarning reports whether the CheckAPIBaseURL warning for baseURL should be printed now: true the first time it is asked for a given base URL in this process, false afterwards. pkg/client and pkg/platform share it, so a sensor that builds several clients for one platform prints the plain-http warning once, not once per client or per request.

func IsIPBlocked

func IsIPBlocked(ip net.IP) bool

IsIPBlocked reports whether the given IP falls in a blocked CIDR. The hard-blocked set always rejects. The RFC1918 / ULA set is conditional on allowPrivate (set via env).

AllowLoopback (test-only) still overrides loopback for httptest servers; it does NOT touch IMDS or RFC1918 — those remain on their own toggle chains.

func IsIPBlockedWith added in v0.7.0

func IsIPBlockedWith(ip net.IP, allowPrivateRanges, allowLoopback bool) bool

IsIPBlockedWith is IsIPBlocked with the two toggles supplied by the caller instead of read from the process-wide env-derived defaults. It lets a component (e.g. the scan-target policy in pkg/core) carry its own allow-private posture without mutating package globals. The hard-blocked set (IMDS/link-local, CGNAT, multicast, unspecified, reserved) is enforced regardless of either toggle; allowLoopback only relaxes 127.0.0.0/8 and ::1.

func NewAPIClient added in v0.7.0

func NewAPIClient(timeout time.Duration) *http.Client

NewAPIClient returns the client for requests that carry an OpenCTEM API key. Every such request goes to the operator-configured API base URL, so the destination is trusted configuration, not attacker input: on-prem and in-cluster platforms live on loopback, RFC1918, ULA or CGNAT (Tailscale) addresses, and SafeHTTPClient's blocklist would refuse all of them. The client still refuses every redirect (the bearer key never follows one), still refuses link-local destinations such as the cloud metadata service and multicast/reserved/unspecified addresses (so a poisoned DNS answer cannot point the key there), and honors HTTP(S)_PROXY / NO_PROXY.

func RefuseRedirects added in v0.7.0

func RefuseRedirects(req *http.Request, _ []*http.Request) error

RefuseRedirects is a CheckRedirect policy that never follows a redirect. Use it for clients that only ever talk to the OpenCTEM API: the API does not issue redirects, so a 3xx means a misconfigured base URL or a hostile intermediary, and following it would forward the bearer key.

func SafeCheckRedirect added in v0.7.0

func SafeCheckRedirect(req *http.Request, via []*http.Request) error

SafeCheckRedirect is the CheckRedirect policy installed on every SafeHTTPClient:

  • at most 10 hops;
  • a redirect may never downgrade https to http (the credential and the response would cross the network in clear text);
  • when a hop changes origin relative to the original request, every header in SensitiveHeaders is removed before the request is sent.

The dialer still applies the IP blocklist to the redirect target, so a redirect into IMDS/loopback fails at connect time.

func SafeHTTPClient

func SafeHTTPClient(timeout time.Duration) *http.Client

SafeHTTPClient returns an *http.Client whose dialer rejects any connection attempt to a blocked CIDR at the transport layer. This is the belt to ValidateURL's braces: even if a caller forgets to validate up front, the dial fails closed.

Callers should still ValidateURL themselves because the dialer-only check happens AFTER DNS resolution, so a request to an attacker- supplied URL will have burned a DNS lookup (possibly against an attacker-controlled resolver). ValidateURL rejects before the lookup leaves the host process.

Types

type ValidationResult

type ValidationResult struct {
	URL         *url.URL
	ResolvedIPs []net.IP
}

ValidationResult carries the parsed URL + the DNS-pinned IP set so callers that want to prevent DNS rebinding can dial one of the resolved IPs rather than re-resolve at dial time.

func ValidateURL

func ValidateURL(rawURL string) (*ValidationResult, error)

ValidateURL parses rawURL, confirms scheme is http/https, blocks common dangerous hostnames, resolves DNS, and rejects if any A/AAAA hits a blocked CIDR. On any failure returns an error — callers MUST NOT proceed to dial.

Fail-closed on DNS lookup failure: if we cannot resolve, we cannot verify the target is safe, so the request is rejected.

Jump to

Keyboard shortcuts

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