ssrf_filter 1.1.1 → 1.1.2

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
  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