ssrf_filter 1.0.5 → 1.0.6

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
2
  SHA1:
3
- metadata.gz: 59270dd5ca4e6fdf5e70fc74e2c1593eea7cd861
4
- data.tar.gz: f23dbbcc57ea0114ae1220e22f93b5f1fa7910da
3
+ metadata.gz: 2e7c6b603a075892984767c0e84bdc31acef8a94
4
+ data.tar.gz: ad328c0f06c37ab4ce7f33610e5f6b16ee0a3537
5
5
  SHA512:
6
- metadata.gz: c6ed09a682cd405c1cf06429b173f87965ff67231bd748a9520ee4c9fe8ed27d84df373abd917c3c25935dfcee984f5cc5c1b4b327548accd3694b6587ce9249
7
- data.tar.gz: 73697be1c1619bda43e8fc7c517380f0f40155de9f20012d79390a58ef9122bec392a443661e0ffae93fb48a04a6aa6bc874b8a7aea9d1a59a1e8c460c317881
6
+ metadata.gz: 553c787de96785842f54c22f4ca9ae85b417ce38139ee93daa95deea1f0ca39383150bc242099d63fa4cea9161508cc4f58e27e06bcc5958bca1d58e7fed1744
7
+ data.tar.gz: f010ffafa1cdf23f45426f33b37eda10df5ad4ac27bef1e4d79851728c2ce3a24e52b62cb8b2c03a11400a0b7db29329672d6be101562d03152df3f0a1641eb5
@@ -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'
@@ -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,48 @@
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
+ @patched_ssl_socket = true
31
+
32
+ ::OpenSSL::SSL::SSLSocket.class_eval do
33
+ original_post_connection_check = instance_method(:post_connection_check)
34
+ define_method(:post_connection_check) do |hostname|
35
+ original_post_connection_check.bind(self).call(::Thread.current[::SsrfFilter::FIBER_LOCAL_KEY] || hostname)
36
+ end
37
+
38
+ if method_defined?(:hostname=)
39
+ original_hostname = instance_method(:hostname=)
40
+ define_method(:hostname=) do |hostname|
41
+ original_hostname.bind(self).call(::Thread.current[::SsrfFilter::FIBER_LOCAL_KEY] || hostname)
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -100,6 +100,9 @@ class SsrfFilter
100
100
 
101
101
  %i[get put post delete].each do |method|
102
102
  define_singleton_method(method) do |url, options = {}, &block|
103
+ ::SsrfFilter::Patch::SSLSocket.apply!
104
+ ::SsrfFilter::Patch::HTTPGenericRequest.apply!
105
+
103
106
  original_url = url
104
107
  scheme_whitelist = options[:scheme_whitelist] || DEFAULT_SCHEME_WHITELIST
105
108
  resolver = options[:resolver] || DEFAULT_RESOLVER
@@ -207,52 +210,7 @@ class SsrfFilter
207
210
  end
208
211
  private_class_method :validate_request
209
212
 
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
213
  def self.with_forced_hostname(hostname, &_block)
255
- patch_ssl_socket!
256
214
  ::Thread.current[FIBER_LOCAL_KEY] = hostname
257
215
  yield
258
216
  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.6'.freeze
5
5
  end
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.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Arkadiy Tetelman
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-01-18 00:00:00.000000000 Z
11
+ date: 2018-02-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler-audit
@@ -58,14 +58,14 @@ dependencies:
58
58
  requirements:
59
59
  - - "~>"
60
60
  - !ruby/object:Gem::Version
61
- version: 3.2.0
61
+ version: 3.3.0
62
62
  type: :development
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
- version: 3.2.0
68
+ version: 3.3.0
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: rubocop
71
71
  requirement: !ruby/object:Gem::Requirement
@@ -88,6 +88,8 @@ extensions: []
88
88
  extra_rdoc_files: []
89
89
  files:
90
90
  - lib/ssrf_filter.rb
91
+ - lib/ssrf_filter/patch/http_generic_request.rb
92
+ - lib/ssrf_filter/patch/ssl_socket.rb
91
93
  - lib/ssrf_filter/ssrf_filter.rb
92
94
  - lib/ssrf_filter/version.rb
93
95
  homepage: https://github.com/arkadiyt/ssrf_filter