ssrf_filter 1.0.5 → 1.0.8

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
- 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: []