ssrf_filter 1.1.0 → 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: 97b1c63593031040d2f23b6fbb055c44490fc2624a5b9901203053c971991ba6
4
- data.tar.gz: ca816401abe0177feea5fd70d01afd19758598cd9864659a8eb97e7184367830
3
+ metadata.gz: 315ab29498751c0bb60964f06932464d8663ec66e50ed403b04c2e19c9fce5e6
4
+ data.tar.gz: 8b3475337b149bf8e04dd11e379e9c0698f74d05528524fea847ed996aff405c
5
5
  SHA512:
6
- metadata.gz: 75ae3f265b486cb8768ef0c49d3dd0313a218b3863a2c1dfc8615727453aea45ab7ae5f71289e3748eecb9e17dc09443870413937c7fbad56a9a7a225b2fbb89
7
- data.tar.gz: c881ea003dafd105bbbff4001646a879fc4cefb18d722fc60f21281c8ebaca8b19a324dccadb9acdf799e56501905addebcfe8f91253ee52136a9dab433335ad
6
+ metadata.gz: 299b91d225b906faa91a1ebd4a8ad32ce46889deaeffa89d5a7488b871310e68ed7f73332c9ee25fd0ff3d01a25dd949907a390e8ce44926910657e2fa054f3e
7
+ data.tar.gz: 89b6051cb2a8f232336e1e25e95a5898c71ba4439f582224452effbcc41b3a68af13d1e16d855f8ebb7634c013fefd3301c661d5d6bf72f3d98894feb867f47f
@@ -3,45 +3,60 @@
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
- original_post_connection_check.bind(self).call(::Thread.current[::SsrfFilter::FIBER_LOCAL_KEY] || hostname)
38
+ original_post_connection_check.bind(self).call(::Thread.current[::SsrfFilter::FIBER_HOSTNAME_KEY] ||
39
+ hostname)
39
40
  end
40
41
 
41
42
  if method_defined?(:hostname=)
42
43
  original_hostname = instance_method(:hostname=)
43
44
  define_method(:hostname=) do |hostname|
44
- original_hostname.bind(self).call(::Thread.current[::SsrfFilter::FIBER_LOCAL_KEY] || hostname)
45
+ original_hostname.bind(self).call(::Thread.current[::SsrfFilter::FIBER_HOSTNAME_KEY] || hostname)
46
+ end
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]
45
60
  end
46
61
  end
47
62
  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 = {
@@ -83,7 +84,7 @@ class SsrfFilter
83
84
  patch: ::Net::HTTP::Patch
84
85
  }.freeze
85
86
 
86
- FIBER_LOCAL_KEY = :__ssrf_filter_hostname
87
+ FIBER_HOSTNAME_KEY = :__ssrf_filter_hostname
87
88
 
88
89
  class Error < ::StandardError
89
90
  end
@@ -108,11 +109,13 @@ class SsrfFilter
108
109
  ::SsrfFilter::Patch::SSLSocket.apply!
109
110
 
110
111
  original_url = url
111
- scheme_whitelist = options[:scheme_whitelist] || DEFAULT_SCHEME_WHITELIST
112
- resolver = options[:resolver] || DEFAULT_RESOLVER
113
- 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)
114
116
  url = url.to_s
115
117
 
118
+ response = nil
116
119
  (max_redirects + 1).times do
117
120
  uri = URI(url)
118
121
 
@@ -131,6 +134,8 @@ class SsrfFilter
131
134
  return response if url.nil?
132
135
  end
133
136
 
137
+ return response if allow_unfollowed_redirects
138
+
134
139
  raise TooManyRedirects, "Got #{max_redirects} redirects fetching #{original_url}"
135
140
  end
136
141
  end
@@ -189,18 +194,18 @@ class SsrfFilter
189
194
 
190
195
  with_forced_hostname(hostname) do
191
196
  ::Net::HTTP.start(uri.hostname, uri.port, **http_options) do |http|
192
- http.request(request) do |response|
193
- case response
194
- when ::Net::HTTPRedirection
195
- url = response['location']
196
- # Handle relative redirects
197
- url = "#{uri.scheme}://#{hostname}:#{uri.port}#{url}" if url.start_with?('/')
198
- return nil, url
199
- else
200
- block&.call(response)
201
- return response, nil
202
- 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
203
207
  end
208
+ return response, url
204
209
  end
205
210
  end
206
211
  end
@@ -220,10 +225,10 @@ class SsrfFilter
220
225
  private_class_method :validate_request
221
226
 
222
227
  def self.with_forced_hostname(hostname, &_block)
223
- ::Thread.current[FIBER_LOCAL_KEY] = hostname
228
+ ::Thread.current[FIBER_HOSTNAME_KEY] = hostname
224
229
  yield
225
230
  ensure
226
- ::Thread.current[FIBER_LOCAL_KEY] = nil
231
+ ::Thread.current[FIBER_HOSTNAME_KEY] = nil
227
232
  end
228
233
  private_class_method :with_forced_hostname
229
234
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class SsrfFilter
4
- VERSION = '1.1.0'
4
+ VERSION = '1.1.2'
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.1.0
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-08-28 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
@@ -167,7 +167,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
167
167
  - !ruby/object:Gem::Version
168
168
  version: '0'
169
169
  requirements: []
170
- rubygems_version: 3.0.3
170
+ rubygems_version: 3.3.7
171
171
  signing_key:
172
172
  specification_version: 4
173
173
  summary: A gem that makes it easy to prevent server side request forgery (SSRF) attacks