Skip to content

Commit 87190f5

Browse files
committed
Add typespecs and clean up documentation
1 parent f07aa0d commit 87190f5

File tree

1 file changed

+166
-51
lines changed

1 file changed

+166
-51
lines changed

lib/safeurl.ex

Lines changed: 166 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,84 @@
11
defmodule SafeURL do
22
@moduledoc """
3-
A library for mitigating Server Side Request Forgery vulnerabilities in Elixir. Private/reserved
4-
IP addresses are blacklisted by default, and users can add additional CIDR ranges to blacklist
5-
or alternatively whitelist specific CIDR ranges to which the application is allowed to make requests.
6-
7-
Examples:
8-
9-
iex(10)> SafeURL.get("https://10.0.0.1/ssrf.txt")
10-
{:error, :restricted}
11-
12-
iex(10)> SafeURL.get("https://google.com/")
13-
{:ok,
14-
%HTTPoison.Response{
15-
body: "{...}",
16-
headers: [
17-
{"..."}
18-
],
19-
request: %HTTPoison.Request{
20-
body: "",
21-
headers: [],
22-
method: :get,
23-
options: [],
24-
params: %{},
25-
url: "https://google.com/"
26-
},
27-
request_url: "https://google.com/",
28-
status_code: 301
29-
}}
3+
`SafeURL` is library for mitigating Server Side Request
4+
Forgery vulnerabilities in Elixir. Private/reserved IP
5+
addresses are blacklisted by default, and users can add
6+
additional CIDR ranges to blacklist, or alternatively
7+
whitelist specific CIDR ranges to which the application is
8+
allowed to make requests.
9+
10+
You can use `allowed?/2` or `validate/2` to check if a
11+
URL is safe to call, or just call it directly via `get/4`
12+
which will validate it automatically before calling, and
13+
return an error if it is not.
14+
15+
16+
## Examples
17+
18+
iex> SafeURL.allowed?("https://includesecurity.com")
19+
true
20+
21+
iex> SafeURL.validate("http://google.com/", schemes: ~w[https])
22+
{:error, :restricted}
23+
24+
iex> SafeURL.validate("http://230.10.10.10/")
25+
{:error, :restricted}
26+
27+
iex> SafeURL.validate("http://230.10.10.10/", blacklist_reserved: false)
28+
:ok
29+
30+
iex> SafeURL.get("https://10.0.0.1/ssrf.txt")
31+
{:error, :restricted}
32+
33+
iex> SafeURL.get("https://google.com/")
34+
{:ok, %HTTPoison.Response{...}}
35+
36+
37+
## Options
38+
39+
`SafeURL` can be configured to customize and override
40+
validation behaviour by passing the following options:
41+
42+
* `:blacklist_reserved` - Blacklist reserved/private IP
43+
ranges. Defaults to `true`.
44+
45+
* `:blacklist` - List of CIDR ranges to blacklist. This is
46+
additive with `:blacklist_reserved`. Defaults to `[]`.
47+
48+
* `:whitelist` - List of CIDR ranges to whitelist. If
49+
specified, blacklists will be ignored. Defaults to `[]`.
50+
51+
* `:schemes` - List of allowed URL schemes. Defaults to
52+
`["http, "https"]`.
53+
54+
If `:blacklist_reserved` is `true` and additional hosts/ranges
55+
are supplied with `:blacklist`, both of them are included in
56+
the final blacklist to validate the address. If whitelisted
57+
ranges are supplied with `:whitelist`, all blacklists are
58+
ignored and any hosts not explicitly declared in the whitelist
59+
are rejected.
60+
61+
These options can be set globally in your `config.exs` file:
62+
63+
config :safeurl,
64+
blacklist_reserved: true,
65+
blacklist: ~w[100.0.0.0/16],
66+
schemes: ~w[https]
67+
68+
Or they can be passed to the function directly, overriding any
69+
global options if set:
70+
71+
iex> SafeURL.validate("http://10.0.0.1/", blacklist_reserved: false)
72+
:ok
73+
74+
iex> SafeURL.validate("https://app.service/", whitelist: ~w[170.0.0.0/24])
75+
:ok
76+
77+
iex> SafeURL.validate("https://app.service/", blacklist: ~w[170.0.0.0/24])
78+
{:error, :restricted}
79+
3080
"""
81+
3182
@reserved_ranges [
3283
"0.0.0.0/8",
3384
"10.0.0.0/8",
@@ -53,43 +104,36 @@ defmodule SafeURL do
53104

54105

55106
@doc """
56-
Validate a URL and execute a GET request using `HTTPoison` with the specified headers and options.
107+
Validate a string URL against a blacklist or whitelist.
57108
58-
Available options and defaults:
59-
* `:blacklist_private` - Blacklist private/reserved IP ranges (default: `true`)
60-
* `:blacklist` - List of CIDR ranges to blacklist. Additive with `:blacklist_private` (default: `nil`)
61-
* `:whitelist` - List of CIDR ranges to whitelist. If specified, blacklists will be ignored (default: `nil`)
62-
* `:schemes` - List of valid URL schemes (default: `["http, "https"]`)
109+
This method checks if a URL is safe to be called by looking at
110+
its scheme and resolved IP address, and matching it against
111+
reserved CIDR ranges, and any provided whitelist/blacklist.
63112
64-
Options specified in `httpoison_options` will be passed directly to `HTTPoison` when the request is executed.
113+
Returns `true` if the URL meets the requirements,
114+
`false` otherwise.
65115
66-
If `:blacklist_private` is `true` and additional hosts/ranges are supplied with `:blacklist`, the
67-
lists are additive. If whitelisted ranges are supplied with `:whitelist`, all blacklists are ignored
68-
and any hosts not explicitly declared in the whitelist are rejected.
116+
## Examples
69117
70-
If the URL is safe, this function returns the `HTTPoison` result directly; otherwise, `{:error, :restricted}`.
71-
"""
72-
def get(url, options \\ [], headers \\ [], httpoison_options \\ []) do
73-
if allowed?(url, options) do
74-
HTTPoison.get(url, headers, httpoison_options)
75-
else
76-
{:error, :restricted}
77-
end
78-
end
118+
iex> SafeURL.allowed?("https://includesecurity.com")
119+
true
79120
121+
iex> SafeURL.allowed?("http://10.0.0.1/")
122+
false
80123
81-
@doc """
82-
Validate a string URL against a blacklist or whitelist.
124+
iex> SafeURL.allowed?("http://10.0.0.1/", whitelist: ~w[10.0.0.0/8])
125+
true
83126
84-
See documentation for `SafeURL.get()` for available options and defaults.
127+
## Options
128+
129+
See [`Options`](#module-options) section above.
85130
86-
Returns `true` if the URL meets the requirements, `false` otherwise.
87131
"""
88132
@spec allowed?(binary(), Keyword.t()) :: boolean()
89133
def allowed?(url, opts \\ []) do
90134
uri = URI.parse(url)
91135
opts = build_options(opts)
92-
ip = resolve_address(uri.host)
136+
address = resolve_address(uri.host)
93137

94138
cond do
95139
uri.scheme not in opts.schemes ->
@@ -104,6 +148,77 @@ defmodule SafeURL do
104148
end
105149

106150

151+
@doc """
152+
Alternative method of validating a URL, returning atoms instead
153+
of booleans.
154+
155+
This calls `allowed?/2` underneath to check if a URL is safe to
156+
be called. If it is, it returns `:ok`, otherwise
157+
`{:error, :restricted}`.
158+
159+
## Examples
160+
161+
iex> SafeURL.validate("https://includesecurity.com")
162+
:ok
163+
164+
iex> SafeURL.validate("http://10.0.0.1/")
165+
{:error, :restricted}
166+
167+
iex> SafeURL.validate("http://10.0.0.1/", whitelist: ~w[10.0.0.0/8])
168+
:ok
169+
170+
## Options
171+
172+
See [`Options`](#module-options) section above.
173+
174+
"""
175+
@spec validate(binary(), Keyword.t()) :: :ok | {:error, :restricted}
176+
def validate(url, opts \\ []) do
177+
if allowed?(url, opts) do
178+
:ok
179+
else
180+
{:error, :restricted}
181+
end
182+
end
183+
184+
185+
@doc """
186+
Validate a URL and execute a GET request using `HTTPoison`.
187+
188+
If the URL is safe, this function will execute the request using
189+
`HTTPoison`, returning the result directly. Otherwise, it will
190+
return `{:error, :restricted}`.
191+
192+
`headers` and `httpoison_options` will be passed directly to
193+
`HTTPoison` when the request is executed.
194+
195+
See `allowed?/2` for more details on URL validation.
196+
197+
## Examples
198+
199+
iex> SafeURL.get("https://10.0.0.1/ssrf.txt")
200+
{:error, :restricted}
201+
202+
iex> SafeURL.get("https://google.com/")
203+
{:ok, %HTTPoison.Response{...}}
204+
205+
iex> SafeURL.get("https://google.com/", schemes: ~w[ftp])
206+
{:error, :restricted}
207+
208+
## Options
209+
210+
See [`Options`](#module-options) section above.
211+
212+
"""
213+
@spec get(binary(), Keyword.t(), HTTPoison.headers(), Keyword.t()) ::
214+
{:ok, HTTPoison.Response.t()} | {:error, :restricted}
215+
def get(url, options \\ [], headers \\ [], httpoison_options \\ []) do
216+
with :ok <- validate(url, options) do
217+
HTTPoison.get(url, headers, httpoison_options)
218+
end
219+
end
220+
221+
107222

108223

109224
# Private Helpers

0 commit comments

Comments
 (0)