PhoenixKit.Modules.Storage.RemoteFetch (phoenix_kit v2.55.1)

Copy Markdown View Source

Downloads a user-supplied URL to a temporary file without letting it reach the server's own network.

A URL is a request for the server to connect somewhere, and the "somewhere" is the attacker's to choose: http://169.254.169.254/ (cloud credentials), http://localhost:5432/, a name that resolves to an internal address, or one that resolves to a public address when checked and an internal one a moment later (DNS rebinding). So:

  • only https by default (allow_http: true to allow plain http), no credentials in the URL, ports 80/443 unless allowed_ports: says so;
  • the host is resolved once, every address it resolves to must be public (PhoenixKit.Utils.PublicAddress), and the connection is made to the address that was checked — with the original name for Host, SNI and certificate verification (Mint's :hostname), so nothing resolves the name again between the check and the connect;
  • redirects are followed by hand (at most max_redirects, default 3), each hop checked again, and an https URL never redirects to http;
  • the body is streamed to a temporary file and cut off at max_bytes (default 25 MB) whatever Content-Length claims, within one overall timeout (default 15 s).

download/2 returns {:ok, %{path: temp_path, filename: name, url: final_url}}; the caller removes the temporary file. What the bytes ARE is decided later, by sniffing them (Storage.store_from_url/2), never from the response's Content-Type.

Summary

Types

reason()

@type reason() ::
  :invalid_url
  | :scheme_not_allowed
  | :port_not_allowed
  | :blocked_host
  | :nxdomain
  | :too_many_redirects
  | :too_large
  | :timeout
  | {:http_status, pos_integer()}
  | {:connect_failed, term()}
  | {:write_failed, term()}

Functions

download(url, opts \\ [])

@spec download(String.t(), keyword()) ::
  {:ok, %{path: String.t(), filename: String.t(), url: String.t()}}
  | {:error, reason()}