ssrf_filter 1.0.5 → 1.0.6

Sign up to get free protection for your applications and to get access to all the features.
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