AttestoPhoenix.ClientIdMetadata.Fetcher.Req (AttestoPhoenix v0.10.0)

Copy Markdown View Source

The default, SSRF-guarded Client ID Metadata Document fetcher - CIMD (draft-ietf-oauth-client-id-metadata-document-01, IETF OAuth WG).

This is the only component of the CIMD feature that makes an outbound request, so it is where the draft's Security Considerations are enforced. Implements AttestoPhoenix.ClientIdMetadata.Fetcher over Req -> Finch -> Mint.

SSRF algorithm (draft Security Considerations)

fetch/2 runs the following, erroring closed at the first failure:

  1. Re-validate the URL is https and satisfies the draft §2 grammar via Attesto.ClientIdMetadata.validate_client_id/1 (defense in depth - the caller is never trusted). Failure -> {:error, {:invalid_url, reason}}.
  2. Resolve the host to A/AAAA records through the injectable :resolver (defaults to :inet.getaddrs/2 for both :inet and :inet6). No records -> {:error, :unresolvable}. The resolver seam lets tests inject addresses without real DNS, and is the hook the DNS-rebinding defense below pins against.
  3. Reject special-use IPs (RFC 6890). Every resolved address is checked against special_use_ip?/1; if any is special-use the fetch is refused with {:error, {:blocked_ip, ip}}. Loopback is permitted only when :allow_loopback is true (development).
  4. Pin to a validated IP. The request is dialed at one checked address (Mint's :hostname connect option keeps TLS SNI, certificate hostname verification, and the Host header on the original hostname while the socket targets the pinned IP), closing the DNS-rebinding TOCTOU between the check in step 3 and the connect: the name cannot be re-resolved to an internal address after it was validated.
  5. GET with Accept: application/json, connect and receive timeouts (:request_timeout_ms, default 5_000), and redirects disabled (draft MUST). Any redirect is surfaced as its 3xx status.
  6. Status must be 200; any other status -> {:error, {:status, n}}.
  7. Content-Type must be application/json or application/<x>+json; otherwise {:error, :bad_content_type}.
  8. Size cap. The body is refused once it exceeds :max_document_bytes (default 5_120) -> {:error, :too_large}.

On success returns {:ok, %{body: body, cache_control: directives}} where directives are the parsed Cache-Control / Expires freshness hints (RFC 9111) for the caller to clamp and store.

Options

  • :resolver - a 2-arity DNS resolver (charlist_host, :inet | :inet6 -> {:ok, [:inet.ip_address()]} | {:error, term()}). Defaults to :inet.getaddrs/2. Injected by tests to exercise the SSRF guard and the DNS-rebinding pin without real DNS.
  • :allow_loopback - when true, loopback addresses (127.0.0.0/8, ::1) are permitted (the draft's "AS runs on loopback" exception). Default false.
  • :max_document_bytes - body size cap. Default 5_120 (draft's recommended 5 KB).
  • :request_timeout_ms - connect and receive timeout. Default 5_000.
  • :req_options - extra options merged into the underlying Req request (e.g. test transport overrides). Escape hatch; not part of the public contract.

Summary

Types

A resolved IP address, as returned by :inet.getaddrs/2.

Functions

Fetch a validated CIMD client_id URL under the SSRF algorithm documented on this module. See AttestoPhoenix.ClientIdMetadata.Fetcher for the contract.

Returns true iff ip is a special-use address (RFC 6890) an authorization server MUST NOT dereference for CIMD: loopback, private, link-local, CGNAT, 0.0.0.0/8, multicast, the reserved/documentation ranges, the IPv6 equivalents (fc00::/7, fe80::/10, ff00::/8, ::1, ::), and every IPv6 form that embeds an IPv4 - IPv4-mapped (::ffff:0:0/96), NAT64 (64:ff9b::/96), 6to4 (2002::/16), and IPv4-compatible (::/96) - which is unwrapped to its embedded IPv4 and re-checked, so an internal IPv4 cannot be smuggled past the guard through any of them.

Types

ip()

@type ip() :: :inet.ip_address()

A resolved IP address, as returned by :inet.getaddrs/2.

Functions

fetch(url, opts \\ [])

@spec fetch(
  String.t(),
  keyword()
) :: {:ok, AttestoPhoenix.ClientIdMetadata.Fetcher.result()} | {:error, term()}

Fetch a validated CIMD client_id URL under the SSRF algorithm documented on this module. See AttestoPhoenix.ClientIdMetadata.Fetcher for the contract.

special_use_ip?(ip)

@spec special_use_ip?(ip()) :: boolean()

Returns true iff ip is a special-use address (RFC 6890) an authorization server MUST NOT dereference for CIMD: loopback, private, link-local, CGNAT, 0.0.0.0/8, multicast, the reserved/documentation ranges, the IPv6 equivalents (fc00::/7, fe80::/10, ff00::/8, ::1, ::), and every IPv6 form that embeds an IPv4 - IPv4-mapped (::ffff:0:0/96), NAT64 (64:ff9b::/96), 6to4 (2002::/16), and IPv4-compatible (::/96) - which is unwrapped to its embedded IPv4 and re-checked, so an internal IPv4 cannot be smuggled past the guard through any of them.

This is the single source of truth for the guard's CIDR table; the :allow_loopback exception is applied by the caller (fetch/2), not here, so this predicate always reports loopback as special-use.