11defmodule 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