AshHooks.Ssrf (AshHooks v3.2.0)

Copy Markdown View Source

Destination validation for outbound webhooks.

Endpoint registration uses registration_safe?/1: it checks the HTTP(S) scheme, known metadata hostnames, and literal IP addresses without DNS. Hostname resolution happens at send time, so registration does not depend on the destination's current DNS availability.

resolve_host_public/2, safe_url?/1, and resolve_public/2 resolve IPv4 and IPv6 addresses and require a nonempty result containing only public addresses. Private, loopback, link-local, unique-local, shared, and reserved ranges are rejected. IPv4-mapped and IPv4-compatible IPv6 addresses are classified using their embedded IPv4 address, as does the NAT64 64:ff9b::/96 prefix. Other IPv6 addresses require an allocated prefix from the IANA IPv6 Global Unicast Address Assignments registry, with non-global special-purpose ranges refused and their globally reachable exceptions retained. Reserved and unallocated space is refused. The allocation table was retrieved on November 13, 2026; new allocations require a package update before admission.

A hostname that fails to resolve fails as :nxdomain when every queried family returned an authoritative negative answer, and as :resolve_timeout otherwise (timeout, temporary resolver failure, or mixed evidence), using the conservative retryable label. safe_url?/1 stays boolean: both are false there.

DNS resolution has a two-second ceiling, shortened by a supplied operation deadline. Both supplied HTTP adapters connect to an address from the validated result while retaining the original hostname for TLS and HTTP.

Callers of resolve_host_public/2 must resolve at every connect, reconnect, redirect, failover, and broker-advertised address. Connect only to an address returned by that call, and keep the original name for TLS verification and server name indication. Resolving again through another connection API would permit DNS rebinding between validation and connection.

Summary

Functions

Checks the scheme, metadata hostnames, and literal IP addresses at registration. This check does not resolve hostnames.

Resolves a host to a nonempty list containing only public IP addresses.

Returns validated addresses behind a URL for connection pinning: {:ok, %{uri: URI, addresses: [ip]}} when every resolved address is public, {:error, :unsafe} when an answer is non-public, and {:error, :nxdomain | :resolve_timeout} when the name does not resolve. The connection target must come from this same result to prevent DNS rebinding between validation and connection.

True when url is an http(s) URL whose host is not metadata and whose resolved addresses are all public.

Functions

registration_safe?(url)

@spec registration_safe?(term()) :: boolean()

Checks the scheme, metadata hostnames, and literal IP addresses at registration. This check does not resolve hostnames.

resolve_host_public(host, opts \\ [])

@spec resolve_host_public(term(), keyword()) ::
  {:ok, [:inet.ip_address()]}
  | {:error, :unsafe | :nxdomain | :resolve_timeout | :invalid_host}

Resolves a host to a nonempty list containing only public IP addresses.

Accepts a DNS name with IDNA-encoded ASCII labels (letters, digits, underscores, and inner hyphens) and at most one trailing dot, a bare IPv4 literal, or a bare or bracketed IPv6 literal. Malformed brackets, zone identifiers, ports, schemes, paths, credentials, whitespace, control characters, empty labels, and other malformed hosts return {:error, :invalid_host} without raising.

Returns {:ok, [ip_address]} only when every resolved address is public. A non-public answer or known metadata hostname returns {:error, :unsafe}. Resolution failures return {:error, :nxdomain | :resolve_timeout}. There is no destination-policy bypass.

DNS resolution has a two-second ceiling. The optional :deadline is an absolute System.monotonic_time(:millisecond) value that can shorten it.

Resolve at every connect, reconnect, redirect, failover, and broker-advertised address. Connect only to a returned address and retain the original name for TLS verification and server name indication.

resolve_public(url, opts \\ [])

@spec resolve_public(term(), keyword()) ::
  {:ok, %{uri: URI.t(), addresses: [:inet.ip_address()]}} | {:error, atom()}

Returns validated addresses behind a URL for connection pinning: {:ok, %{uri: URI, addresses: [ip]}} when every resolved address is public, {:error, :unsafe} when an answer is non-public, and {:error, :nxdomain | :resolve_timeout} when the name does not resolve. The connection target must come from this same result to prevent DNS rebinding between validation and connection.

safe_url?(url)

@spec safe_url?(term()) :: boolean()

True when url is an http(s) URL whose host is not metadata and whose resolved addresses are all public.