ssrf_filter 1.1.0 → 1.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/ssrf_filter/patch/ssl_socket.rb +41 -26
- data/lib/ssrf_filter/ssrf_filter.rb +22 -17
- data/lib/ssrf_filter/version.rb +1 -1
- metadata +7 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 315ab29498751c0bb60964f06932464d8663ec66e50ed403b04c2e19c9fce5e6
|
4
|
+
data.tar.gz: 8b3475337b149bf8e04dd11e379e9c0698f74d05528524fea847ed996aff405c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 299b91d225b906faa91a1ebd4a8ad32ce46889deaeffa89d5a7488b871310e68ed7f73332c9ee25fd0ff3d01a25dd949907a390e8ce44926910657e2fa054f3e
|
7
|
+
data.tar.gz: 89b6051cb2a8f232336e1e25e95a5898c71ba4439f582224452effbcc41b3a68af13d1e16d855f8ebb7634c013fefd3301c661d5d6bf72f3d98894feb867f47f
|
@@ -3,45 +3,60 @@
|
|
3
3
|
class SsrfFilter
|
4
4
|
module Patch
|
5
5
|
module SSLSocket
|
6
|
-
# When fetching a url we'd like to have the following workflow:
|
7
|
-
# 1) resolve the hostname www.example.com, and choose a public ip address to connect to
|
8
|
-
# 2) connect to that specific ip address, to prevent things like DNS TOCTTOU bugs or other trickery
|
9
|
-
#
|
10
|
-
# Ideally this would happen by the ruby http library giving us control over DNS resolution,
|
11
|
-
# but it doesn't. Instead, when making the request we set the uri.hostname to the chosen ip address,
|
12
|
-
# and send a 'Host' header of the original hostname, i.e. connect to 'http://93.184.216.34/' and send
|
13
|
-
# a 'Host: www.example.com' header.
|
14
|
-
#
|
15
|
-
# This works for the http case, http://www.example.com. For the https case, this causes certificate
|
16
|
-
# validation failures, since the server certificate for https://www.example.com will not have a
|
17
|
-
# Subject Alternate Name for 93.184.216.34.
|
18
|
-
#
|
19
|
-
# Thus we perform the monkey-patch below, modifying SSLSocket's `post_connection_check(hostname)`
|
20
|
-
# and `hostname=(hostname)` methods:
|
21
|
-
# If our fiber local variable is set, use that for the hostname instead, otherwise behave as usual.
|
22
|
-
# The only time the variable will be set is if you are executing inside a `with_forced_hostname` block,
|
23
|
-
# which is used in ssrf_filter.rb.
|
24
|
-
#
|
25
|
-
# An alternative approach could be to pass in our own OpenSSL::X509::Store with a custom
|
26
|
-
# `verify_callback` to the ::Net::HTTP.start call, but this would require reimplementing certification
|
27
|
-
# validation, which is dangerous. This way we can piggyback on the existing validation and simply pretend
|
28
|
-
# that we connected to the desired hostname.
|
29
|
-
|
30
6
|
def self.apply!
|
31
7
|
return if instance_variable_defined?(:@patched_ssl_socket)
|
32
8
|
|
33
9
|
@patched_ssl_socket = true
|
34
10
|
|
35
11
|
::OpenSSL::SSL::SSLSocket.class_eval do
|
12
|
+
# When fetching a url we'd like to have the following workflow:
|
13
|
+
# 1) resolve the hostname www.example.com, and choose a public ip address to connect to
|
14
|
+
# 2) connect to that specific ip address, to prevent things like DNS TOCTTOU bugs or other trickery
|
15
|
+
#
|
16
|
+
# Ideally this would happen by the ruby http library giving us control over DNS resolution,
|
17
|
+
# but it doesn't. Instead, when making the request we set the uri.hostname to the chosen ip address,
|
18
|
+
# and send a 'Host' header of the original hostname, i.e. connect to 'http://93.184.216.34/' and send
|
19
|
+
# a 'Host: www.example.com' header.
|
20
|
+
#
|
21
|
+
# This works for the http case, http://www.example.com. For the https case, this causes certificate
|
22
|
+
# validation failures, since the server certificate for https://www.example.com will not have a
|
23
|
+
# Subject Alternate Name for 93.184.216.34.
|
24
|
+
#
|
25
|
+
# Thus we perform the monkey-patch below, modifying SSLSocket's `post_connection_check(hostname)`
|
26
|
+
# and `hostname=(hostname)` methods:
|
27
|
+
# If our fiber local variable is set, use that for the hostname instead, otherwise behave as usual.
|
28
|
+
# The only time the variable will be set is if you are executing inside a `with_forced_hostname` block,
|
29
|
+
# which is used in ssrf_filter.rb.
|
30
|
+
#
|
31
|
+
# An alternative approach could be to pass in our own OpenSSL::X509::Store with a custom
|
32
|
+
# `verify_callback` to the ::Net::HTTP.start call, but this would require reimplementing certification
|
33
|
+
# validation, which is dangerous. This way we can piggyback on the existing validation and simply pretend
|
34
|
+
# that we connected to the desired hostname.
|
35
|
+
|
36
36
|
original_post_connection_check = instance_method(:post_connection_check)
|
37
37
|
define_method(:post_connection_check) do |hostname|
|
38
|
-
original_post_connection_check.bind(self).call(::Thread.current[::SsrfFilter::
|
38
|
+
original_post_connection_check.bind(self).call(::Thread.current[::SsrfFilter::FIBER_HOSTNAME_KEY] ||
|
39
|
+
hostname)
|
39
40
|
end
|
40
41
|
|
41
42
|
if method_defined?(:hostname=)
|
42
43
|
original_hostname = instance_method(:hostname=)
|
43
44
|
define_method(:hostname=) do |hostname|
|
44
|
-
original_hostname.bind(self).call(::Thread.current[::SsrfFilter::
|
45
|
+
original_hostname.bind(self).call(::Thread.current[::SsrfFilter::FIBER_HOSTNAME_KEY] || hostname)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# This patch is the successor to https://github.com/arkadiyt/ssrf_filter/pull/54
|
50
|
+
# Due to some changes in Ruby's net/http library (namely https://github.com/ruby/net-http/pull/36),
|
51
|
+
# the SSLSocket in the request was no longer getting `s.hostname` set.
|
52
|
+
# The original fix tried to monkey-patch the Regexp class to cause the original code path to execute,
|
53
|
+
# but this caused other problems (like https://github.com/arkadiyt/ssrf_filter/issues/61)
|
54
|
+
# This fix attempts a different approach to set the hostname on the socket
|
55
|
+
original_initialize = instance_method(:initialize)
|
56
|
+
define_method(:initialize) do |*args|
|
57
|
+
original_initialize.bind(self).call(*args)
|
58
|
+
if ::Thread.current.key?(::SsrfFilter::FIBER_HOSTNAME_KEY)
|
59
|
+
self.hostname = ::Thread.current[::SsrfFilter::FIBER_HOSTNAME_KEY]
|
45
60
|
end
|
46
61
|
end
|
47
62
|
end
|
@@ -72,6 +72,7 @@ class SsrfFilter
|
|
72
72
|
::Resolv.getaddresses(hostname).map { |ip| ::IPAddr.new(ip) }
|
73
73
|
end
|
74
74
|
|
75
|
+
DEFAULT_ALLOW_UNFOLLOWED_REDIRECTS = false
|
75
76
|
DEFAULT_MAX_REDIRECTS = 10
|
76
77
|
|
77
78
|
VERB_MAP = {
|
@@ -83,7 +84,7 @@ class SsrfFilter
|
|
83
84
|
patch: ::Net::HTTP::Patch
|
84
85
|
}.freeze
|
85
86
|
|
86
|
-
|
87
|
+
FIBER_HOSTNAME_KEY = :__ssrf_filter_hostname
|
87
88
|
|
88
89
|
class Error < ::StandardError
|
89
90
|
end
|
@@ -108,11 +109,13 @@ class SsrfFilter
|
|
108
109
|
::SsrfFilter::Patch::SSLSocket.apply!
|
109
110
|
|
110
111
|
original_url = url
|
111
|
-
scheme_whitelist = options
|
112
|
-
resolver = options
|
113
|
-
|
112
|
+
scheme_whitelist = options.fetch(:scheme_whitelist, DEFAULT_SCHEME_WHITELIST)
|
113
|
+
resolver = options.fetch(:resolver, DEFAULT_RESOLVER)
|
114
|
+
allow_unfollowed_redirects = options.fetch(:allow_unfollowed_redirects, DEFAULT_ALLOW_UNFOLLOWED_REDIRECTS)
|
115
|
+
max_redirects = options.fetch(:max_redirects, DEFAULT_MAX_REDIRECTS)
|
114
116
|
url = url.to_s
|
115
117
|
|
118
|
+
response = nil
|
116
119
|
(max_redirects + 1).times do
|
117
120
|
uri = URI(url)
|
118
121
|
|
@@ -131,6 +134,8 @@ class SsrfFilter
|
|
131
134
|
return response if url.nil?
|
132
135
|
end
|
133
136
|
|
137
|
+
return response if allow_unfollowed_redirects
|
138
|
+
|
134
139
|
raise TooManyRedirects, "Got #{max_redirects} redirects fetching #{original_url}"
|
135
140
|
end
|
136
141
|
end
|
@@ -189,18 +194,18 @@ class SsrfFilter
|
|
189
194
|
|
190
195
|
with_forced_hostname(hostname) do
|
191
196
|
::Net::HTTP.start(uri.hostname, uri.port, **http_options) do |http|
|
192
|
-
http.request(request) do |
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
end
|
197
|
+
response = http.request(request) do |res|
|
198
|
+
block&.call(res)
|
199
|
+
end
|
200
|
+
case response
|
201
|
+
when ::Net::HTTPRedirection
|
202
|
+
url = response['location']
|
203
|
+
# Handle relative redirects
|
204
|
+
url = "#{uri.scheme}://#{hostname}:#{uri.port}#{url}" if url.start_with?('/')
|
205
|
+
else
|
206
|
+
url = nil
|
203
207
|
end
|
208
|
+
return response, url
|
204
209
|
end
|
205
210
|
end
|
206
211
|
end
|
@@ -220,10 +225,10 @@ class SsrfFilter
|
|
220
225
|
private_class_method :validate_request
|
221
226
|
|
222
227
|
def self.with_forced_hostname(hostname, &_block)
|
223
|
-
::Thread.current[
|
228
|
+
::Thread.current[FIBER_HOSTNAME_KEY] = hostname
|
224
229
|
yield
|
225
230
|
ensure
|
226
|
-
::Thread.current[
|
231
|
+
::Thread.current[FIBER_HOSTNAME_KEY] = nil
|
227
232
|
end
|
228
233
|
private_class_method :with_forced_hostname
|
229
234
|
end
|
data/lib/ssrf_filter/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ssrf_filter
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.1.
|
4
|
+
version: 1.1.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Arkadiy Tetelman
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2023-09-01 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler-audit
|
@@ -44,14 +44,14 @@ dependencies:
|
|
44
44
|
requirements:
|
45
45
|
- - "~>"
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version: 3.
|
47
|
+
version: 3.12.0
|
48
48
|
type: :development
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
52
|
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
|
-
version: 3.
|
54
|
+
version: 3.12.0
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
56
|
name: rubocop
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -86,14 +86,14 @@ dependencies:
|
|
86
86
|
requirements:
|
87
87
|
- - "~>"
|
88
88
|
- !ruby/object:Gem::Version
|
89
|
-
version: 0.
|
89
|
+
version: 0.22.0
|
90
90
|
type: :development
|
91
91
|
prerelease: false
|
92
92
|
version_requirements: !ruby/object:Gem::Requirement
|
93
93
|
requirements:
|
94
94
|
- - "~>"
|
95
95
|
- !ruby/object:Gem::Version
|
96
|
-
version: 0.
|
96
|
+
version: 0.22.0
|
97
97
|
- !ruby/object:Gem::Dependency
|
98
98
|
name: simplecov-lcov
|
99
99
|
requirement: !ruby/object:Gem::Requirement
|
@@ -167,7 +167,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
167
167
|
- !ruby/object:Gem::Version
|
168
168
|
version: '0'
|
169
169
|
requirements: []
|
170
|
-
rubygems_version: 3.
|
170
|
+
rubygems_version: 3.3.7
|
171
171
|
signing_key:
|
172
172
|
specification_version: 4
|
173
173
|
summary: A gem that makes it easy to prevent server side request forgery (SSRF) attacks
|