Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prepare 1.0.0 #13

Merged
merged 13 commits into from
Jan 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .formatter.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]
8 changes: 4 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ jobs:
strategy:
matrix:
include:
- elixir: '1.10'
otp: '21'
- elixir: '1.16'
otp: '26'
- elixir: '1.14'
otp: '23'
- elixir: '1.17'
otp: '27'

steps:
- uses: actions/checkout@v2
Expand Down
36 changes: 12 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ through SafeURL:
```elixir
def deps do
[
{:safeurl, "~> 0.3"},
{:httpoison, "~> 1.8"}, # Optional
{:safeurl, "~> 1.0"},
{:httpoison, "~> 2.2"}, # Optional
]
end
```
Expand All @@ -49,20 +49,20 @@ iex> SafeURL.allowed?("https://includesecurity.com")
true

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

iex> SafeURL.validate("http://230.10.10.10/")
{:error, :restricted}
{:error, :unsafe_reserved}

iex> SafeURL.validate("http://230.10.10.10/", block_reserved: false)
:ok

# When HTTPoison is available:

iex> SafeURL.get("https://10.0.0.1/ssrf.txt")
{:error, :restricted}
iex> SafeURL.HTTPoison.get("https://10.0.0.1/ssrf.txt")
{:error, :unsafe_reserved}

iex> SafeURL.get("https://google.com/")
iex> SafeURL.HTTPoison.get("https://google.com/")
{:ok, %HTTPoison.Response{...}}
```

Expand All @@ -86,6 +86,8 @@ following options:
- `:dns_module` - Any module that implements the `SafeURL.DNSResolver` behaviour.
Defaults to `DNS` from the [`:dns`][lib-dns] package.

- `:detailed_error` - Return specific error if validation fails. If set to `false`, `validate/2` will return `{:error, :restricted}` regardless of the reason. Defaults to `true`.

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

Expand All @@ -103,7 +105,7 @@ Find detailed documentation on [HexDocs][docs].

## HTTP Clients

While SafeURL already provides a convenient [`get/4`][docs-get] method to validate hosts
While SafeURL already provides a convenient [`SafeURL.HTTPoison.get/3`][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.

Expand Down Expand Up @@ -137,29 +139,15 @@ iex> CustomClient.get("http://230.10.10.10/data.json", [], safeurl: [block_reser

### Tesla

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

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

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

And you can plug it in anywhere you're using Tesla:
For [Tesla][lib-tesla], `SafeURL` provides a helper middleware out-of-the-box, which you can plug anywhere you're using `Tesla`:

```elixir
defmodule DocumentService do
use Tesla

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

def fetch(id) do
get("/documents/#{id}")
Expand Down
30 changes: 30 additions & 0 deletions guides/migrating_to_1.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Migrating from 0.3 to 1.0

### Replace `SafeURL.get/4` with `SafeURL.HTTPoison.get/3`

`SafeURL.get/4` has been replaced with `SafeURL.HTTPoison.get/3`. It is now a
drop-in replacement for `HTTPoison.get/3` and thus does not support passing
`SafeURL` options through function arguments.

### Account for new error messages or use `:detailed_error` option

`SafeURL.validate/2` now returns a more specific error by default:

```elixir
iex> SafeURL.validate("http://localhost")
{:error, :unsafe_reserved}
iex> SafeURL.validate("http://google.com", schemes: [:https])
{:error, :unsafe_scheme}
```

You can use the `:detailed_error` configuration option to restore the previous
behavior and get the generic `{:error, :restricted}` error:

```elixir
iex> SafeURL.validate("http://localhost", detailed_error: false)
{:error, :restricted}
iex> Application.put_env(:safeurl, :detailed_error, false)
:ok
iex> SafeURL.validate("http://localhost")
{:error, :restricted}
```
40 changes: 40 additions & 0 deletions lib/safeurl/httpoison.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
if Code.ensure_loaded?(HTTPoison) do
defmodule SafeURL.HTTPoison do
@moduledoc since: "1.0.0"
@moduledoc """
A utility module that should be a drop-in replacement for `HTTPoison`. Only
supports `get/3`.
"""

@doc since: "1.0.0"
@doc """
Validate a URL and execute a GET request using `HTTPoison`.

If the URL is safe, this function will execute the request using
`HTTPoison`, returning the result directly. Otherwise, it will
return error.

`headers` and `options` will be passed directly to
`HTTPoison` when the request is executed.

## Examples

iex> SafeURL.HTTPoison.get("https://10.0.0.1/ssrf.txt")
{:error, :unsafe_reserved}

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

"""
@spec get(binary(), HTTPoison.headers(), Keyword.t()) ::
{:ok, HTTPoison.Response.t()}
| {:error, HTTPoison.Error.t()}
| {:error, SafeURL.error()}
| {:error, :restricted}
def get(url, headers \\ [], options \\ []) do
with :ok <- SafeURL.validate(url) do
HTTPoison.get(url, headers, options)
end
end
end
end
Loading
Loading