ReqSSRF (ReqSSRF v0.1.0)

Copy Markdown View Source

Decides whether the server may fetch a URL that somebody else chose.

A server that fetches a user-given URL can be pointed at the internal network, e.g. at another internal service, a database admin page, or the cloud provider's metadata endpoint. This module validates a URL against a list of reserved IP address ranges. attach/2 ensures that every redirect hop of a Req request is validated.

The documentation of public_address?/1 lists the reserved ranges. See README for more details on what the library does and does not do.

Telemetry

attach/2 emits an event when it refuses a URL. Nothing is emitted for a URL that passes.

A rising rate of blocked URLs means somebody is probing the endpoint.

  • [:req_ssrf, :blocked]
    • measurements: %{system_time: System.system_time()}
    • metadata: %{url: URI.t(), reason: t:reason/0}

Summary

Types

Options for check/2 and allowed?/2.

The reason why a URL may not be fetched.

A function that resolves a name to addresses of one family.

Functions

Returns true if the URL may be fetched and false otherwise.

Checks the URL of every hop of a Req request.

Returns :ok if the URL may be fetched, or {:error, reason} if it may not.

Returns true if the given IP address is one the internet can route to and false otherwise.

Types

opts()

@type opts() :: [
  allow_ip_address: boolean(),
  deny: [String.t()],
  resolver: resolver(),
  schemes: [String.t()],
  timeout: timeout()
]

Options for check/2 and allowed?/2.

  • :allow_ip_address - whether a host written as an IP address is accepted. Defaults to true.
  • :deny - additional address ranges to refuse, as a list of CIDR strings. Defaults to [].
  • :resolver - the function that resolves a name, for tests that must not touch the network. It is called as resolver.(charlist, family, timeout) with the family :inet or :inet6, and returns what :inet.getaddrs/3 returns. Defaults to &:inet.getaddrs/3, which is also the contract to mirror. There is no reason to set it in production.
  • :schemes - the accepted URL schemes. Defaults to ["http", "https"].
  • :timeout - how long to wait for a name to resolve, in milliseconds, or :infinity. Applies to each address family, so a host that answers for neither takes twice as long. Defaults to 2000.

reason()

@type reason() ::
  :invalid_url
  | :missing_host
  | :unsupported_scheme
  | :ip_address
  | :unresolvable_host
  | :resolution_failed
  | :reserved_address
  | :denied_address

The reason why a URL may not be fetched.

resolver()

@type resolver() :: (charlist(), :inet | :inet6, timeout() ->
                 {:ok, [:inet.ip_address()]} | {:error, term()})

A function that resolves a name to addresses of one family.

Mirrors :inet.getaddrs/3.

Functions

allowed?(url, opts \\ [])

@spec allowed?(String.t() | URI.t(), opts()) :: boolean()

Returns true if the URL may be fetched and false otherwise.

Same as check/2, but returns a boolean.

Examples

iex> allowed?("http://8.8.8.8/")
true

iex> allowed?("http://127.0.0.1/")
false

attach(request, opts \\ [])

@spec attach(Req.Request.t(), opts()) :: Req.Request.t()

Checks the URL of every hop of a Req request.

The check runs as a request step appended after the steps that build the URL, so it sees the URL that is about to be requested, and Req re-runs it for each redirect.

This function must be called after any other step that can change the URL.

A request whose URL may not be fetched is halted with ReqSSRF.BlockedError. Req.request/2 returns {:error, %ReqSSRF.BlockedError{}} and Req.request!/2 raises it.

Takes the same options as check/2.

The :ssrf_check request option decides what a single request does:

  • true - run the check with the options given to attach/2. This is the default.
  • false - skip the check for this request.
  • a keyword list - run the check with these options on top of the ones given to attach/2, so an option that is not named keeps its attached value.

Any other value raises ArgumentError, since a value that is neither of the above would otherwise skip the check silently.

Examples

Req.new()
|> ReqSSRF.attach(allow_ip_address: false)
|> Req.get(url: submitted_url)

Skipping the check for one request:

Req.get(request, url: internal_url, ssrf_check: false)

Allowing an IP address for one request, keeping every other attached option:

Req.get(request, url: url, ssrf_check: [allow_ip_address: true])

check(url, opts \\ [])

@spec check(String.t() | URI.t(), opts()) :: :ok | {:error, reason()}

Returns :ok if the URL may be fetched, or {:error, reason} if it may not.

If you use Req, use attach/2 instead. This ensures that Req checks every URL when following redirects. If you use a different HTTP client that follows redirects, you must check every URL that is redirected to yourself.

Examples

iex> check("http://8.8.8.8/")
:ok

iex> check("http://169.254.169.254/latest/meta-data/")
{:error, :reserved_address}

iex> check("file:///etc/passwd")
{:error, :unsupported_scheme}

iex> check("http://8.8.8.8/", allow_ip_address: false)
{:error, :ip_address}

public_address?(address)

@spec public_address?(:inet.ip_address()) :: boolean()

Returns true if the given IP address is one the internet can route to and false otherwise.

An IPv4-mapped IPv6 address points to the same host as its IPv4 form and is matched against the IPv4 ranges.

Reserved ranges

An IPv4 address is refused if it falls in one of the IANA special-purpose ranges that the internet cannot route to. 168.63.129.16/32 among them is Azure's host endpoint. The registry also lists globally routed anycast ranges, such as the AS112 and AMT assignments, and those are not refused.

  • 0.0.0.0/8
  • 10.0.0.0/8
  • 100.64.0.0/10
  • 127.0.0.0/8
  • 168.63.129.16/32
  • 169.254.0.0/16
  • 172.16.0.0/12
  • 192.0.0.0/24
  • 192.0.2.0/24
  • 192.88.99.0/24
  • 192.168.0.0/16
  • 198.18.0.0/15
  • 198.51.100.0/24
  • 203.0.113.0/24
  • 224.0.0.0/4
  • 240.0.0.0/4

For IPv6, 2000::/3 is the only range IANA has allocated for global unicast, so an address outside it is refused without enumerating anything. These special-purpose ranges inside it are refused as well.

  • 2001::/23
  • 2001:db8::/32
  • 2002::/16
  • 3fff::/20

Examples

iex> public_address?({8, 8, 8, 8})
true

iex> public_address?({169, 254, 169, 254})
false

iex> public_address?({0, 0, 0, 0, 0, 0xFFFF, 0xA9FE, 0xA9FE})
false