<!--[![Build Status][badge-github]][github-build]-->

> SSRF Protection in Elixir 🛡️

SafeURL is a library that aids developers in protecting against a class of vulnerabilities
known as Server Side Request Forgery. It does this by validating a URL against a configurable
allow or block list before making an HTTP request. SafeURL is open-source and licensed under

This library was originally created by Nick Fox at [Include Security][includesecurity],
with substantial improvements contributed by the [Slab][slab] team. As of January 2022, this
library is now officially maintained by Slab.

See the [Documentation][docs] on HexDocs.


## Installation

To get started, add `safeurl` to your project dependencies in `mix.exs`. Optionally, you may
also add [`HTTPoison`][lib-httpoison] to your dependencies for making requests directly
through SafeURL:

def deps do
    {:safeurl, "~> 0.3.0"},
    {:httpoison, "~> 1.8"},  # Optional

To use SafeURL with your favorite HTTP Client, see the [HTTP Clients][readme-http] section.


## Usage

`SafeURL` blocks private/reserved IP addresses are by default, and users can add additional
CIDR ranges to the blocklist, or alternatively allow specific CIDR ranges to which the
application is allowed to make requests.

You can use `allowed?/2` or `validate/2` to check if a URL is safe to call. If you have the
[`HTTPoison`][lib-httpoison] application available, you can also call `get/4` which will
validate the host automatically before making a web request, and return an error otherwise.

iex> SafeURL.allowed?("")

iex> SafeURL.validate("", schemes: ~w[https])
{:error, :restricted}

iex> SafeURL.validate("")
{:error, :restricted}

iex> SafeURL.validate("", block_reserved: false)

# When HTTPoison is available:

iex> SafeURL.get("")
{:error, :restricted}

iex> SafeURL.get("")
{:ok, %HTTPoison.Response{...}}


## Configuration

`SafeURL` can be configured to customize and override validation behaviour by passing the
following options:

  * `:block_reserved` - Block reserved/private IP ranges. Defaults to `true`.

  * `:blocklist` - List of CIDR ranges to block. This is additive with `:block_reserved`.
    Defaults to `[]`.

  * `:allowlist` - List of CIDR ranges to allow. If specified, blocklist will be ignored.
    Defaults to `[]`.

  * `:schemes` - List of allowed URL schemes. Defaults to `["http, "https"]`.

  * `:dns_module` - Any module that implements the `SafeURL.DNSResolver` behaviour.
    Defaults to `DNS` from the [`:dns`][lib-dns] package.

These options can be passed to the function directly or set globally in your `config.exs`

config :safeurl,
  block_reserved: true,
  blocklist: ~w[],
  schemes: ~w[https],
  dns_module: MyCustomDNSResolver

Find detailed documentation on [HexDocs][docs].


## HTTP Clients

While SafeURL already provides a convenient [`get/4`][docs-get] method to validate hosts
before making GET HTTP requests, you can also write your own wrappers, helpers or
middleware to work with the HTTP Client of your choice.

### HTTPoison

For [HTTPoison][lib-httpoison], you can create a wrapper module that validates hosts
before making HTTP requests:

defmodule CustomClient do
  def request(method, url, body, headers \\ [], opts \\ []) do
    {safeurl_opts, opts} = Keyword.pop(opts, :safeurl, [])

    with :ok <- SafeURL.validate(url, safeurl_opts) do
      HTTPoison.request(method, url, body, headers, opts)

  def get(url, headers \\ [], opts \\ []),        do: request(:get, url, "", headers, opts)
  def post(url, body, headers \\ [], opts \\ []), do: request(:post, url, body, headers, opts)
  # ...

And you can use it as:

iex> CustomClient.get("", [], safeurl: [block_reserved: false], recv_timeout: 500)
{:ok, %HTTPoison.Response{...}}

### Tesla

For [Tesla][lib-tesla], you can write a custom middleware to halt requests that are not

defmodule MyApp.Middleware.SafeURL do
  @behaviour Tesla.Middleware

  @impl true
  def call(env, next, opts) do
    with :ok <- SafeURL.validate(env.url, opts), do:

And you can plug it in anywhere you're using Tesla:

defmodule DocumentService do
  use Tesla

  plug Tesla.Middleware.BaseUrl, "https://document-service/"
  plug Tesla.Middleware.JSON
  plug MyApp.Middleware.SafeURL, schemes: ~w[https], allowlist: [""]

  def fetch(id) do


## Custom DNS Resolver

In some cases you might want to use a custom strategy for DNS resolution. You can do so by
passing your own implementation of [`SafeURL.DNSResolver`][docs-dns] in the global or local

Example use-cases of this are:

 - Using a specific DNS server
 - Avoiding network access in specific environments
 - Mocking DNS resolution in tests

You can do so by implementing `DNSResolver`:

defmodule TestDNSResolver do
  @behaviour SafeURL.DNSResolver

  @impl true
  def resolve(""), do: {:ok, [{192, 168, 1, 10}]}
  def resolve(""), do: {:ok, [{192, 168, 1, 20}]}
  def resolve(_domain),      do: {:ok, [{192, 168, 1, 99}]}

config :safeurl, dns_module: TestDNSResolver

For more examples, see [`SafeURL.DNSResolver`][docs-dns] docs.


## Contributing

 - [Fork][github-fork], Enhance, Send PR
 - Lock issues with any bugs or feature requests
 - Implement something from Roadmap
 - Spread the word :heart:


## License

This package is available as open source under the terms of the [MIT License][github-license].



[readme-http]:      #http-clients