@@ -8,9 +8,7 @@ defmodule SafeURL do
8
8
allowed to make requests.
9
9
10
10
You can use `allowed?/2` or `validate/2` to check if a
11
- URL is safe to call. If the `HTTPoison` application is
12
- available, you can also call `get/4` directly which will
13
- validate the host before making an HTTP request.
11
+ URL is safe to call.
14
12
15
13
16
14
## Examples
@@ -19,18 +17,18 @@ defmodule SafeURL do
19
17
true
20
18
21
19
iex> SafeURL.validate("http://google.com/", schemes: ~w[https])
22
- {:error, :restricted }
20
+ {:error, :unsafe_scheme }
23
21
24
22
iex> SafeURL.validate("http://230.10.10.10/")
25
- {:error, :restricted }
23
+ {:error, :unsafe_reserved }
26
24
27
25
iex> SafeURL.validate("http://230.10.10.10/", block_reserved: false)
28
26
:ok
29
27
30
28
# If HTTPoison is available:
31
29
32
30
iex> SafeURL.HTTPoison.get("https://10.0.0.1/ssrf.txt")
33
- {:error, :restricted }
31
+ {:error, :unsafe_reserved }
34
32
35
33
iex> SafeURL.HTTPoison.get("https://google.com/")
36
34
{:ok, %HTTPoison.Response{...}}
@@ -57,6 +55,10 @@ defmodule SafeURL do
57
55
`SafeURL.DNSResolver` behaviour. Defaults to `DNS` from
58
56
the `:dns` package.
59
57
58
+ * `:detailed_error` - Return specific error if validation fails. If set to
59
+ `false`, `validate/2` will return `{:error, :restricted}` regardless of
60
+ the reason. Defaults to `true`.
61
+
60
62
61
63
If `:block_reserved` is `true` and additional hosts/ranges
62
64
are supplied with `:blocklist`, both of them are included in
@@ -105,12 +107,11 @@ defmodule SafeURL do
105
107
"240.0.0.0/4"
106
108
]
107
109
108
-
110
+ @ type error ( ) :: :unsafe_scheme | :unsafe_allowlist | :unsafe_blocklist | :unsafe_reserved
109
111
110
112
# Public API
111
113
# ----------
112
114
113
-
114
115
@ doc """
115
116
Validate a string URL against a blocklist or allowlist.
116
117
@@ -139,29 +140,19 @@ defmodule SafeURL do
139
140
"""
140
141
@ spec allowed? ( binary ( ) , Keyword . t ( ) ) :: boolean ( )
141
142
def allowed? ( url , opts \\ [ ] ) do
142
- uri = URI . parse ( url )
143
- opts = build_options ( opts )
144
- address = resolve_address ( uri . host , opts . dns_module )
145
-
146
- cond do
147
- uri . scheme not in opts . schemes ->
148
- false
149
-
150
- opts . allowlist != [ ] ->
151
- ip_in_ranges? ( address , opts . allowlist )
152
-
153
- true ->
154
- ! ip_in_ranges? ( address , opts . blocklist )
143
+ case validate ( url , opts ) do
144
+ :ok -> true
145
+ { :error , _ } -> false
155
146
end
156
147
end
157
148
158
-
159
149
@ doc """
160
- Alternative method of validating a URL, returning atoms instead
150
+ Alternative method of validating a URL, returning result tuple instead
161
151
of booleans.
162
152
163
153
This calls `allowed?/2` underneath to check if a URL is safe to
164
- be called. If it is, it returns `:ok`, otherwise
154
+ be called. If it is, it returns `:ok`, otherwise an error tuple with a
155
+ specific reason. If `:detailed_error` is set to `false`, the error is always
165
156
`{:error, :restricted}`.
166
157
167
158
## Examples
@@ -180,48 +171,61 @@ defmodule SafeURL do
180
171
See [`Options`](#module-options) section above.
181
172
182
173
"""
183
- @ spec validate ( binary ( ) , Keyword . t ( ) ) :: :ok | { :error , :restricted }
174
+ @ spec validate ( binary ( ) , Keyword . t ( ) ) :: :ok | { :error , error ( ) | :restricted }
184
175
def validate ( url , opts \\ [ ] ) do
185
- if allowed? ( url , opts ) do
186
- :ok
187
- else
188
- { :error , :restricted }
176
+ uri = URI . parse ( url )
177
+ opts = build_options ( opts )
178
+ address = resolve_address ( uri . host , opts . dns_module )
179
+
180
+ result =
181
+ cond do
182
+ uri . scheme not in opts . schemes ->
183
+ { :error , :unsafe_scheme }
184
+
185
+ opts . allowlist != [ ] ->
186
+ if ip_in_ranges? ( address , opts . allowlist ) , do: :ok , else: { :error , :unsafe_allowlist }
187
+
188
+ opts . blocklist != [ ] and ip_in_ranges? ( address , opts . blocklist ) ->
189
+ { :error , :unsafe_blocklist }
190
+
191
+ opts . block_reserved and ip_in_ranges? ( address , @ reserved_ranges ) ->
192
+ { :error , :unsafe_reserved }
193
+
194
+ true ->
195
+ :ok
196
+ end
197
+
198
+ with { :error , _ } <- result do
199
+ if opts . detailed_error , do: result , else: { :error , :restricted }
189
200
end
190
201
end
191
202
192
-
193
203
# Private Helpers
194
204
# ---------------
195
205
196
-
197
206
# Return a map of calculated options
198
207
defp build_options ( opts ) do
199
208
schemes = get_option ( opts , :schemes )
200
209
allowlist = get_option ( opts , :allowlist )
201
210
blocklist = get_option ( opts , :blocklist )
202
211
dns_module = get_option ( opts , :dns_module )
203
-
204
- blocklist =
205
- if get_option ( opts , :block_reserved ) do
206
- blocklist ++ @ reserved_ranges
207
- else
208
- blocklist
209
- end
210
-
211
- % { schemes: schemes , allowlist: allowlist , blocklist: blocklist , dns_module: dns_module }
212
+ block_reserved = get_option ( opts , :block_reserved )
213
+ detailed_error = get_option ( opts , :detailed_error )
214
+
215
+ % {
216
+ schemes: schemes ,
217
+ allowlist: allowlist ,
218
+ blocklist: blocklist ,
219
+ dns_module: dns_module ,
220
+ block_reserved: block_reserved ,
221
+ detailed_error: detailed_error
222
+ }
212
223
end
213
224
214
-
215
225
# Get the value of a specific option, either from the application
216
226
# configs or overrides explicitly passed as arguments.
217
- defp get_option ( opts , key ) do
218
- if Keyword . has_key? ( opts , key ) do
219
- Keyword . get ( opts , key )
220
- else
221
- Application . get_env ( :safeurl , key )
222
- end
223
- end
224
-
227
+ defp get_option ( opts , key ) ,
228
+ do: Keyword . get_lazy ( opts , key , fn -> Application . get_env ( :safeurl , key ) end )
225
229
226
230
# Resolve hostname in DNS to an IP address (if not already an IP)
227
231
defp resolve_address ( hostname , dns_module ) do
@@ -241,7 +245,6 @@ defmodule SafeURL do
241
245
end
242
246
end
243
247
244
-
245
248
defp ip_in_ranges? ( { _ , _ , _ , _ } = addr , ranges ) when is_list ( ranges ) do
246
249
Enum . any? ( ranges , fn range ->
247
250
range
0 commit comments