DomainConnect.HostGuard (DomainConnect v0.5.0)

Copy Markdown View Source

SSRF guard for the host taken from a domain's _domainconnect TXT record.

Discovery fetches https://<host>/v2/<zone>/settings, and <host> is supplied by whoever controls the target domain's DNS. Without a guard, a caller who can submit an arbitrary domain could make this library request internal addresses.

check/2 enforces, before any HTTP request is made:

  • the value is a syntactically valid DNS hostname (not an IP literal, no port, userinfo, path, scheme, or whitespace), and
  • every address it resolves to is publicly routable — loopback, private (RFC 1918), link-local, unique-local (IPv6 ULA), CGNAT, and multicast/reserved ranges are rejected.

The address resolver is injectable via the :address_resolver option so the guard can be unit-tested without real DNS, and so callers can supply a hardened resolver.

Residual risk

This checks the addresses now; the HTTP client re-resolves when it connects, so a DNS-rebinding attacker could still point at a private address between the check and the connection. Run discovery from a network segment that cannot reach internal services for defence in depth.

Summary

Functions

Returns :ok if host is a safe public hostname, else {:error, reason}.

Like check/2, but takes a full URL and checks its host. Used to guard the urlAPI endpoint (served in the discovered settings, hence DNS-influenced) before this library makes an OAuth/apply request to it.

Types

reason()

@type reason() :: :invalid_host | :ip_literal | :unresolvable | :blocked_ip

Functions

check(host, opts \\ [])

@spec check(
  term(),
  keyword()
) :: :ok | {:error, reason()}

Returns :ok if host is a safe public hostname, else {:error, reason}.

Options

  • :address_resolver — a 1-arity fun (host :: String.t()) -> {:ok, [:inet.ip_address()]} | {:error, term()}. Defaults to an :inet.getaddrs-backed resolver (IPv4 + IPv6).

check_url(url, opts \\ [])

@spec check_url(
  term(),
  keyword()
) :: :ok | {:error, reason()}

Like check/2, but takes a full URL and checks its host. Used to guard the urlAPI endpoint (served in the discovered settings, hence DNS-influenced) before this library makes an OAuth/apply request to it.