ssrf_filter 1.0.5 → 1.0.8

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 59270dd5ca4e6fdf5e70fc74e2c1593eea7cd861
4
- data.tar.gz: f23dbbcc57ea0114ae1220e22f93b5f1fa7910da
2
+ SHA256:
3
+ metadata.gz: 3ad08c7683abaf9c1cce31358e91eebd54e2276911da99c3d7bdbc1481ec6a8f
4
+ data.tar.gz: 4993290726f69399264c18dfe8b4ef1c908b78c726f0cfb5dcbc5b9c09043ead
5
5
  SHA512:
6
- metadata.gz: c6ed09a682cd405c1cf06429b173f87965ff67231bd748a9520ee4c9fe8ed27d84df373abd917c3c25935dfcee984f5cc5c1b4b327548accd3694b6587ce9249
7
- data.tar.gz: 73697be1c1619bda43e8fc7c517380f0f40155de9f20012d79390a58ef9122bec392a443661e0ffae93fb48a04a6aa6bc874b8a7aea9d1a59a1e8c460c317881
6
+ metadata.gz: 522368253a813feb3ac735f419b8d9fe6ac3b006debf3e3c003f6909d809b5b51af06ccc9c42b7cd8d18e90d54a298d3a39fee0514304a1c3a2a1ffd1d54580d
7
+ data.tar.gz: 21cd1fe841b77536db7e51e9cddc94b7ed96305bf766bb16df77e5b83029a6b6c44e042d1036ae52a135419e72ae70ec1155c471fdc6596eb9dab07809e54e36
@@ -0,0 +1,83 @@
1
+ require 'stringio'
2
+
3
+ class SsrfFilter
4
+ module Patch
5
+ module HTTPGenericRequest
6
+ # Ruby had a bug in its Http library where if you set a custom `Host` header on a request it would get
7
+ # overwritten. This was tracked in:
8
+ # https://bugs.ruby-lang.org/issues/10054
9
+ # and resolved with the commit:
10
+ # https://github.com/ruby/ruby/commit/70a2eb63999265ff7e8d46d1f5b410c8ee3d30d7#diff-5c08b4ae27d2294a8294a27ff9af4a85
11
+ # Versions of Ruby that don't have this fix applied will fail to connect to certain hosts via SsrfFilter. The
12
+ # patch below backports the fix from the linked commit.
13
+
14
+ def self.should_apply?
15
+ # Check if the patch needs to be applied:
16
+ # The Ruby bug was that HTTPGenericRequest#exec overwrote the Host header, so this snippet checks
17
+ # if we can reproduce that behavior. It does not actually open any network connections.
18
+
19
+ uri = URI('https://www.example.com')
20
+ request = ::Net::HTTPGenericRequest.new('HEAD', false, false, uri)
21
+ request['host'] = '127.0.0.1'
22
+ request.exec(StringIO.new, '1.1', '/')
23
+ request['host'] == uri.hostname
24
+ end
25
+
26
+ # Apply the patch from the linked commit onto ::Net::HTTPGenericRequest. Since this is 3rd party code,
27
+ # disable code coverage and rubocop linting for this section.
28
+ # :nocov:
29
+ # rubocop:disable all
30
+ def self.apply!
31
+ return if instance_variable_defined?(:@checked_http_generic_request)
32
+ @checked_http_generic_request = true
33
+ return unless should_apply?
34
+
35
+ ::Net::HTTPGenericRequest.class_eval do
36
+ def exec(sock, ver, path)
37
+ if @body
38
+ send_request_with_body sock, ver, path, @body
39
+ elsif @body_stream
40
+ send_request_with_body_stream sock, ver, path, @body_stream
41
+ elsif @body_data
42
+ send_request_with_body_data sock, ver, path, @body_data
43
+ else
44
+ write_header sock, ver, path
45
+ end
46
+ end
47
+
48
+ def update_uri(addr, port, ssl)
49
+ # reflect the connection and @path to @uri
50
+ return unless @uri
51
+
52
+ if ssl
53
+ scheme = 'https'.freeze
54
+ klass = URI::HTTPS
55
+ else
56
+ scheme = 'http'.freeze
57
+ klass = URI::HTTP
58
+ end
59
+
60
+ if host = self['host']
61
+ host.sub!(/:.*/s, ''.freeze)
62
+ elsif host = @uri.host
63
+ else
64
+ host = addr
65
+ end
66
+ # convert the class of the URI
67
+ if @uri.is_a?(klass)
68
+ @uri.host = host
69
+ @uri.port = port
70
+ else
71
+ @uri = klass.new(
72
+ scheme, @uri.userinfo,
73
+ host, port, nil,
74
+ @uri.path, nil, @uri.query, nil)
75
+ end
76
+ end
77
+ end
78
+ end
79
+ # rubocop:enable all
80
+ # :nocov:
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,49 @@
1
+ class SsrfFilter
2
+ module Patch
3
+ module SSLSocket
4
+ # When fetching a url we'd like to have the following workflow:
5
+ # 1) resolve the hostname www.example.com, and choose a public ip address to connect to
6
+ # 2) connect to that specific ip address, to prevent things like DNS TOCTTOU bugs or other trickery
7
+ #
8
+ # Ideally this would happen by the ruby http library giving us control over DNS resolution,
9
+ # but it doesn't. Instead, when making the request we set the uri.hostname to the chosen ip address,
10
+ # and send a 'Host' header of the original hostname, i.e. connect to 'http://93.184.216.34/' and send
11
+ # a 'Host: www.example.com' header.
12
+ #
13
+ # This works for the http case, http://www.example.com. For the https case, this causes certificate
14
+ # validation failures, since the server certificate for https://www.example.com will not have a
15
+ # Subject Alternate Name for 93.184.216.34.
16
+ #
17
+ # Thus we perform the monkey-patch below, modifying SSLSocket's `post_connection_check(hostname)`
18
+ # and `hostname=(hostname)` methods:
19
+ # If our fiber local variable is set, use that for the hostname instead, otherwise behave as usual.
20
+ # The only time the variable will be set is if you are executing inside a `with_forced_hostname` block,
21
+ # which is used in ssrf_filter.rb.
22
+ #
23
+ # An alternative approach could be to pass in our own OpenSSL::X509::Store with a custom
24
+ # `verify_callback` to the ::Net::HTTP.start call, but this would require reimplementing certification
25
+ # validation, which is dangerous. This way we can piggyback on the existing validation and simply pretend
26
+ # that we connected to the desired hostname.
27
+
28
+ def self.apply!
29
+ return if instance_variable_defined?(:@patched_ssl_socket)
30
+
31
+ @patched_ssl_socket = true
32
+
33
+ ::OpenSSL::SSL::SSLSocket.class_eval do
34
+ original_post_connection_check = instance_method(:post_connection_check)
35
+ define_method(:post_connection_check) do |hostname|
36
+ original_post_connection_check.bind(self).call(::Thread.current[::SsrfFilter::FIBER_LOCAL_KEY] || hostname)
37
+ end
38
+
39
+ if method_defined?(:hostname=)
40
+ original_hostname = instance_method(:hostname=)
41
+ define_method(:hostname=) do |hostname|
42
+ original_hostname.bind(self).call(::Thread.current[::SsrfFilter::FIBER_LOCAL_KEY] || hostname)
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -53,11 +53,12 @@ class SsrfFilter
53
53
  ::IPAddr.new('2002::/16'), # 6to4
54
54
  ::IPAddr.new('fc00::/7'), # Unique local address
55
55
  ::IPAddr.new('fe80::/10'), # Link-local address
56
- ::IPAddr.new('ff00::/8'), # Multicast
56
+ ::IPAddr.new('ff00::/8') # Multicast
57
57
  ] + IPV4_BLACKLIST.flat_map do |ipaddr|
58
58
  prefixlen = prefixlen_from_ipaddr(ipaddr)
59
59
 
60
- ipv4_compatible = ipaddr.ipv4_compat.mask(96 + prefixlen)
60
+ # Don't call ipaddr.ipv4_compat because it prints out a deprecation warning on ruby 2.5+
61
+ ipv4_compatible = IPAddr.new(ipaddr.to_i, Socket::AF_INET6).mask(96 + prefixlen)
61
62
  ipv4_mapped = ipaddr.ipv4_mapped.mask(80 + prefixlen)
62
63
 
63
64
  [ipv4_compatible, ipv4_mapped]
@@ -75,7 +76,9 @@ class SsrfFilter
75
76
  get: ::Net::HTTP::Get,
76
77
  put: ::Net::HTTP::Put,
77
78
  post: ::Net::HTTP::Post,
78
- delete: ::Net::HTTP::Delete
79
+ delete: ::Net::HTTP::Delete,
80
+ head: ::Net::HTTP::Head,
81
+ patch: ::Net::HTTP::Patch
79
82
  }.freeze
80
83
 
81
84
  FIBER_LOCAL_KEY = :__ssrf_filter_hostname
@@ -98,8 +101,11 @@ class SsrfFilter
98
101
  class CRLFInjection < Error
99
102
  end
100
103
 
101
- %i[get put post delete].each do |method|
104
+ %i[get put post delete head patch].each do |method|
102
105
  define_singleton_method(method) do |url, options = {}, &block|
106
+ ::SsrfFilter::Patch::SSLSocket.apply!
107
+ ::SsrfFilter::Patch::HTTPGenericRequest.apply!
108
+
103
109
  original_url = url
104
110
  scheme_whitelist = options[:scheme_whitelist] || DEFAULT_SCHEME_WHITELIST
105
111
  resolver = options[:resolver] || DEFAULT_RESOLVER
@@ -185,9 +191,11 @@ class SsrfFilter
185
191
  block.call(request) if block_given?
186
192
  validate_request(request)
187
193
 
188
- use_ssl = uri.scheme == 'https'
194
+ http_options = options[:http_options] || {}
195
+ http_options[:use_ssl] = (uri.scheme == 'https')
196
+
189
197
  with_forced_hostname(hostname) do
190
- ::Net::HTTP.start(uri.hostname, uri.port, use_ssl: use_ssl) do |http|
198
+ ::Net::HTTP.start(uri.hostname, uri.port, http_options) do |http|
191
199
  http.request(request)
192
200
  end
193
201
  end
@@ -207,52 +215,7 @@ class SsrfFilter
207
215
  end
208
216
  private_class_method :validate_request
209
217
 
210
- def self.patch_ssl_socket!
211
- return if instance_variable_defined?(:@patched_ssl_socket)
212
-
213
- # What we'd like to do is have the following workflow:
214
- # 1) resolve the hostname www.example.com, and choose a public ip address to connect to
215
- # 2) connect to that specific ip address, to prevent things like DNS TOCTTOU bugs or other trickery
216
- #
217
- # Ideally this would happen by the ruby http library giving us control over DNS resolution,
218
- # but it doesn't. Instead, when making the request we set the uri.hostname to the chosen ip address,
219
- # and send a 'Host' header of the original hostname, i.e. connect to 'http://93.184.216.34/' and send
220
- # a 'Host: www.example.com' header.
221
- #
222
- # This works for the http case, http://www.example.com. For the https case, this causes certificate
223
- # validation failures, since the server certificate does not have a Subject Alternate Name for 93.184.216.34.
224
- #
225
- # Thus we perform the monkey-patch below, modifying SSLSocket's `post_connection_check(hostname)`
226
- # and `hostname=(hostname)` methods:
227
- # If our fiber local variable is set, use that for the hostname instead, otherwise behave as usual.
228
- # The only time the variable will be set is if you are executing inside a `with_forced_hostname` block,
229
- # which is used above.
230
- #
231
- # An alternative approach could be to pass in our own OpenSSL::X509::Store with a custom
232
- # `verify_callback` to the ::Net::HTTP.start call, but this would require reimplementing certification
233
- # validation, which is dangerous. This way we can piggyback on the existing validation and simply pretend
234
- # that we connected to the desired hostname.
235
-
236
- ::OpenSSL::SSL::SSLSocket.class_eval do
237
- original_post_connection_check = instance_method(:post_connection_check)
238
- define_method(:post_connection_check) do |hostname|
239
- original_post_connection_check.bind(self).call(::Thread.current[FIBER_LOCAL_KEY] || hostname)
240
- end
241
-
242
- if method_defined?(:hostname=)
243
- original_hostname = instance_method(:hostname=)
244
- define_method(:hostname=) do |hostname|
245
- original_hostname.bind(self).call(::Thread.current[FIBER_LOCAL_KEY] || hostname)
246
- end
247
- end
248
- end
249
-
250
- @patched_ssl_socket = true
251
- end
252
- private_class_method :patch_ssl_socket!
253
-
254
218
  def self.with_forced_hostname(hostname, &_block)
255
- patch_ssl_socket!
256
219
  ::Thread.current[FIBER_LOCAL_KEY] = hostname
257
220
  yield
258
221
  ensure
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class SsrfFilter
4
- VERSION = '1.0.5'.freeze
4
+ VERSION = '1.0.8'.freeze
5
5
  end
data/lib/ssrf_filter.rb CHANGED
@@ -1,4 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'ssrf_filter/patch/http_generic_request'
4
+ require 'ssrf_filter/patch/ssl_socket'
3
5
  require 'ssrf_filter/ssrf_filter'
4
6
  require 'ssrf_filter/version'
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.0.5
4
+ version: 1.0.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Arkadiy Tetelman
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-01-18 00:00:00.000000000 Z
11
+ date: 2022-08-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler-audit
@@ -16,85 +16,143 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: 0.6.0
19
+ version: 0.6.1
20
20
  type: :development
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: 0.6.0
26
+ version: 0.6.1
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: coveralls
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: 0.8.0
33
+ version: 0.8.22
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: 0.8.0
40
+ version: 0.8.22
41
+ - !ruby/object:Gem::Dependency
42
+ name: psych
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "<"
46
+ - !ruby/object:Gem::Version
47
+ version: '4'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "<"
53
+ - !ruby/object:Gem::Version
54
+ version: '4'
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: rspec
43
57
  requirement: !ruby/object:Gem::Requirement
44
58
  requirements:
45
59
  - - "~>"
46
60
  - !ruby/object:Gem::Version
47
- version: 3.7.0
61
+ version: 3.8.0
48
62
  type: :development
49
63
  prerelease: false
50
64
  version_requirements: !ruby/object:Gem::Requirement
51
65
  requirements:
52
66
  - - "~>"
53
67
  - !ruby/object:Gem::Version
54
- version: 3.7.0
68
+ version: 3.8.0
55
69
  - !ruby/object:Gem::Dependency
56
70
  name: webmock
57
71
  requirement: !ruby/object:Gem::Requirement
58
72
  requirements:
59
- - - "~>"
73
+ - - ">="
60
74
  - !ruby/object:Gem::Version
61
- version: 3.2.0
75
+ version: 3.5.1
62
76
  type: :development
63
77
  prerelease: false
64
78
  version_requirements: !ruby/object:Gem::Requirement
65
79
  requirements:
66
- - - "~>"
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: 3.5.1
83
+ - !ruby/object:Gem::Dependency
84
+ name: webrick
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: public_suffix
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - '='
102
+ - !ruby/object:Gem::Version
103
+ version: 2.0.5
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - '='
109
+ - !ruby/object:Gem::Version
110
+ version: 2.0.5
111
+ - !ruby/object:Gem::Dependency
112
+ name: rexml
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - '='
116
+ - !ruby/object:Gem::Version
117
+ version: 3.2.4
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - '='
67
123
  - !ruby/object:Gem::Version
68
- version: 3.2.0
124
+ version: 3.2.4
69
125
  - !ruby/object:Gem::Dependency
70
126
  name: rubocop
71
127
  requirement: !ruby/object:Gem::Requirement
72
128
  requirements:
73
129
  - - "~>"
74
130
  - !ruby/object:Gem::Version
75
- version: 0.52.0
131
+ version: 0.50.0
76
132
  type: :development
77
133
  prerelease: false
78
134
  version_requirements: !ruby/object:Gem::Requirement
79
135
  requirements:
80
136
  - - "~>"
81
137
  - !ruby/object:Gem::Version
82
- version: 0.52.0
138
+ version: 0.50.0
83
139
  description: A gem that makes it easy to prevent server side request forgery (SSRF)
84
140
  attacks
85
- email:
141
+ email:
86
142
  executables: []
87
143
  extensions: []
88
144
  extra_rdoc_files: []
89
145
  files:
90
146
  - lib/ssrf_filter.rb
147
+ - lib/ssrf_filter/patch/http_generic_request.rb
148
+ - lib/ssrf_filter/patch/ssl_socket.rb
91
149
  - lib/ssrf_filter/ssrf_filter.rb
92
150
  - lib/ssrf_filter/version.rb
93
151
  homepage: https://github.com/arkadiyt/ssrf_filter
94
152
  licenses:
95
153
  - MIT
96
154
  metadata: {}
97
- post_install_message:
155
+ post_install_message:
98
156
  rdoc_options: []
99
157
  require_paths:
100
158
  - lib
@@ -109,9 +167,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
109
167
  - !ruby/object:Gem::Version
110
168
  version: '0'
111
169
  requirements: []
112
- rubyforge_project:
113
- rubygems_version: 2.6.14
114
- signing_key:
170
+ rubygems_version: 3.2.15
171
+ signing_key:
115
172
  specification_version: 4
116
173
  summary: A gem that makes it easy to prevent server side request forgery (SSRF) attacks
117
174
  test_files: []