ssrf_filter 1.1.1 → 1.1.2

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
  SHA256:
3
- metadata.gz: f0230e20c7cd24b24dad277ee01042f3a4885fa68a8ea593fe0607de92023026
4
- data.tar.gz: 6ce71a83907b1acfc382acc5d859a26fc1471dc3cd2dbc48d14b3e1dfca12051
3
+ metadata.gz: 315ab29498751c0bb60964f06932464d8663ec66e50ed403b04c2e19c9fce5e6
4
+ data.tar.gz: 8b3475337b149bf8e04dd11e379e9c0698f74d05528524fea847ed996aff405c
5
5
  SHA512:
6
- metadata.gz: cb56468fb4bdcf10168efbc18f004d996363b9dcc8877a1fab1eff508ef1519b86c7eeb750fedd8b565a162804746f9a036b54e584520137c1c2b1a86d5c9421
7
- data.tar.gz: 9d5e0ea62956d551caa2474a384fa90e827c6d133bbfe04831f77dd49cabe5bcb3abc7008eefc63bccedf1634a33bcc27f8178253ec26f7e051075dcfba7066e
6
+ metadata.gz: 299b91d225b906faa91a1ebd4a8ad32ce46889deaeffa89d5a7488b871310e68ed7f73332c9ee25fd0ff3d01a25dd949907a390e8ce44926910657e2fa054f3e
7
+ data.tar.gz: 89b6051cb2a8f232336e1e25e95a5898c71ba4439f582224452effbcc41b3a68af13d1e16d855f8ebb7634c013fefd3301c661d5d6bf72f3d98894feb867f47f
@@ -3,36 +3,36 @@
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
38
  original_post_connection_check.bind(self).call(::Thread.current[::SsrfFilter::FIBER_HOSTNAME_KEY] ||
@@ -45,6 +45,20 @@ class SsrfFilter
45
45
  original_hostname.bind(self).call(::Thread.current[::SsrfFilter::FIBER_HOSTNAME_KEY] || hostname)
46
46
  end
47
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]
60
+ end
61
+ end
48
62
  end
49
63
  end
50
64
  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 = {
@@ -84,7 +85,6 @@ class SsrfFilter
84
85
  }.freeze
85
86
 
86
87
  FIBER_HOSTNAME_KEY = :__ssrf_filter_hostname
87
- FIBER_ADDRESS_KEY = :__ssrf_filter_address
88
88
 
89
89
  class Error < ::StandardError
90
90
  end
@@ -107,14 +107,15 @@ class SsrfFilter
107
107
  %i[get put post delete head patch].each do |method|
108
108
  define_singleton_method(method) do |url, options = {}, &block|
109
109
  ::SsrfFilter::Patch::SSLSocket.apply!
110
- ::SsrfFilter::Patch::Resolv.apply!
111
110
 
112
111
  original_url = url
113
- scheme_whitelist = options[:scheme_whitelist] || DEFAULT_SCHEME_WHITELIST
114
- resolver = options[:resolver] || DEFAULT_RESOLVER
115
- max_redirects = options[:max_redirects] || DEFAULT_MAX_REDIRECTS
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)
116
116
  url = url.to_s
117
117
 
118
+ response = nil
118
119
  (max_redirects + 1).times do
119
120
  uri = URI(url)
120
121
 
@@ -133,6 +134,8 @@ class SsrfFilter
133
134
  return response if url.nil?
134
135
  end
135
136
 
137
+ return response if allow_unfollowed_redirects
138
+
136
139
  raise TooManyRedirects, "Got #{max_redirects} redirects fetching #{original_url}"
137
140
  end
138
141
  end
@@ -189,20 +192,20 @@ class SsrfFilter
189
192
  http_options = options[:http_options] || {}
190
193
  http_options[:use_ssl] = (uri.scheme == 'https')
191
194
 
192
- with_forced_hostname(hostname, ip) do
195
+ with_forced_hostname(hostname) do
193
196
  ::Net::HTTP.start(uri.hostname, uri.port, **http_options) do |http|
194
- http.request(request) do |response|
195
- case response
196
- when ::Net::HTTPRedirection
197
- url = response['location']
198
- # Handle relative redirects
199
- url = "#{uri.scheme}://#{hostname}:#{uri.port}#{url}" if url.start_with?('/')
200
- return nil, url
201
- else
202
- block&.call(response)
203
- return response, nil
204
- 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
205
207
  end
208
+ return response, url
206
209
  end
207
210
  end
208
211
  end
@@ -221,13 +224,11 @@ class SsrfFilter
221
224
  end
222
225
  private_class_method :validate_request
223
226
 
224
- def self.with_forced_hostname(hostname, ip, &_block)
227
+ def self.with_forced_hostname(hostname, &_block)
225
228
  ::Thread.current[FIBER_HOSTNAME_KEY] = hostname
226
- ::Thread.current[FIBER_ADDRESS_KEY] = ip
227
229
  yield
228
230
  ensure
229
231
  ::Thread.current[FIBER_HOSTNAME_KEY] = nil
230
- ::Thread.current[FIBER_ADDRESS_KEY] = nil
231
232
  end
232
233
  private_class_method :with_forced_hostname
233
234
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class SsrfFilter
4
- VERSION = '1.1.1'
4
+ VERSION = '1.1.2'
5
5
  end
data/lib/ssrf_filter.rb CHANGED
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'ssrf_filter/patch/resolv'
4
3
  require 'ssrf_filter/patch/ssl_socket'
5
4
  require 'ssrf_filter/ssrf_filter'
6
5
  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.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: 2022-09-01 00:00:00.000000000 Z
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.11.0
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.11.0
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.21.2
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.21.2
96
+ version: 0.22.0
97
97
  - !ruby/object:Gem::Dependency
98
98
  name: simplecov-lcov
99
99
  requirement: !ruby/object:Gem::Requirement
@@ -144,7 +144,6 @@ extensions: []
144
144
  extra_rdoc_files: []
145
145
  files:
146
146
  - lib/ssrf_filter.rb
147
- - lib/ssrf_filter/patch/resolv.rb
148
147
  - lib/ssrf_filter/patch/ssl_socket.rb
149
148
  - lib/ssrf_filter/ssrf_filter.rb
150
149
  - lib/ssrf_filter/version.rb
@@ -168,7 +167,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
168
167
  - !ruby/object:Gem::Version
169
168
  version: '0'
170
169
  requirements: []
171
- rubygems_version: 3.0.3
170
+ rubygems_version: 3.3.7
172
171
  signing_key:
173
172
  specification_version: 4
174
173
  summary: A gem that makes it easy to prevent server side request forgery (SSRF) attacks
@@ -1,44 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'resolv'
4
-
5
- class SsrfFilter
6
- module Patch
7
- module Resolv
8
- # As described in ssl_socket.rb, we want to patch ruby's http connection code to allow us to make outbound network
9
- # requests while ensuring that both:
10
- # 1) we're connecting to a public / non-private ip address
11
- # 2) https connections continue to work
12
- #
13
- # This used to work fine prior to this change in ruby's net/http library:
14
- # https://github.com/ruby/net-http/pull/36
15
- # After this changed was introduced our patch no longer works - we need to set the hostname to the correct
16
- # value on the SSLSocket (`s.hostname = ssl_host_address`), but that code path no longer executes due to the
17
- # modification in the linked pull request.
18
- #
19
- # To work around this we introduce the patch below, which forces our ip address string to not match against the
20
- # Resolv IPv4/IPv6 regular expressions. This is ugly and cumbersome but I didn't see any better path.
21
- class PatchedRegexp < Regexp
22
- def ===(other)
23
- if ::Thread.current.key?(::SsrfFilter::FIBER_ADDRESS_KEY) &&
24
- other.object_id.equal?(::Thread.current[::SsrfFilter::FIBER_ADDRESS_KEY].object_id)
25
- false
26
- else
27
- super(other)
28
- end
29
- end
30
- end
31
-
32
- def self.apply!
33
- return if instance_variable_defined?(:@patched_resolv)
34
-
35
- @patched_resolv = true
36
-
37
- old_ipv4 = ::Resolv::IPv4.send(:remove_const, :Regex)
38
- old_ipv6 = ::Resolv::IPv6.send(:remove_const, :Regex)
39
- ::Resolv::IPv4.const_set(:Regex, PatchedRegexp.new(old_ipv4))
40
- ::Resolv::IPv6.const_set(:Regex, PatchedRegexp.new(old_ipv6))
41
- end
42
- end
43
- end
44
- end