ssrf_filter 1.1.2 → 1.2.0

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: 315ab29498751c0bb60964f06932464d8663ec66e50ed403b04c2e19c9fce5e6
4
- data.tar.gz: 8b3475337b149bf8e04dd11e379e9c0698f74d05528524fea847ed996aff405c
3
+ metadata.gz: c7b6cb5db50edebd286cc411415c02c7e461815da7e21b75d5cc2d0780b149cb
4
+ data.tar.gz: e48564883db3e5db9b7e0d54a134680fe035f8f33f8f74a378dc75448545347a
5
5
  SHA512:
6
- metadata.gz: 299b91d225b906faa91a1ebd4a8ad32ce46889deaeffa89d5a7488b871310e68ed7f73332c9ee25fd0ff3d01a25dd949907a390e8ce44926910657e2fa054f3e
7
- data.tar.gz: 89b6051cb2a8f232336e1e25e95a5898c71ba4439f582224452effbcc41b3a68af13d1e16d855f8ebb7634c013fefd3301c661d5d6bf72f3d98894feb867f47f
6
+ metadata.gz: cdc1bb1de979857312fe2d59d3b09c03ce3a58a63b768414bac40bba56e7ed87fc003a08249d0bc5df04328067aa914528cbffb41cf7bd1b648b531f36e44776
7
+ data.tar.gz: 8e733db18680051554b6a69148088837880a86a62a31c6427e2131966653bd9ebd4e5957e2ea42d8e58260eb474bdd5a00bde1776a25d679fadab65bbfbb6853
@@ -10,7 +10,7 @@ class SsrfFilter
10
10
  mask_addr = ipaddr.instance_variable_get('@mask_addr')
11
11
  raise ArgumentError, 'Invalid mask' if mask_addr.zero?
12
12
 
13
- while (mask_addr & 0x1).zero?
13
+ while mask_addr.nobits?(0x1)
14
14
  mask_addr >>= 1
15
15
  end
16
16
 
@@ -84,8 +84,6 @@ class SsrfFilter
84
84
  patch: ::Net::HTTP::Patch
85
85
  }.freeze
86
86
 
87
- FIBER_HOSTNAME_KEY = :__ssrf_filter_hostname
88
-
89
87
  class Error < ::StandardError
90
88
  end
91
89
 
@@ -106,8 +104,6 @@ class SsrfFilter
106
104
 
107
105
  %i[get put post delete head patch].each do |method|
108
106
  define_singleton_method(method) do |url, options = {}, &block|
109
- ::SsrfFilter::Patch::SSLSocket.apply!
110
-
111
107
  original_url = url
112
108
  scheme_whitelist = options.fetch(:scheme_whitelist, DEFAULT_SCHEME_WHITELIST)
113
109
  resolver = options.fetch(:resolver, DEFAULT_RESOLVER)
@@ -156,16 +152,16 @@ class SsrfFilter
156
152
  end
157
153
  private_class_method :ipaddr_has_mask?
158
154
 
159
- def self.host_header(hostname, uri)
155
+ def self.normalized_hostname(uri)
160
156
  # Attach port for non-default as per RFC2616
161
157
  if (uri.port == 80 && uri.scheme == 'http') ||
162
158
  (uri.port == 443 && uri.scheme == 'https')
163
- hostname
159
+ uri.hostname
164
160
  else
165
- "#{hostname}:#{uri.port}"
161
+ "#{uri.hostname}:#{uri.port}"
166
162
  end
167
163
  end
168
- private_class_method :host_header
164
+ private_class_method :normalized_hostname
169
165
 
170
166
  def self.fetch_once(uri, ip, verb, options, &block)
171
167
  if options[:params]
@@ -174,11 +170,8 @@ class SsrfFilter
174
170
  uri.query = ::URI.encode_www_form(params)
175
171
  end
176
172
 
177
- hostname = uri.hostname
178
- uri.hostname = ip
179
-
180
173
  request = VERB_MAP[verb].new(uri)
181
- request['host'] = host_header(hostname, uri)
174
+ request['host'] = normalized_hostname(uri)
182
175
 
183
176
  Array(options[:headers]).each do |header, value|
184
177
  request[header] = value
@@ -189,24 +182,24 @@ class SsrfFilter
189
182
  options[:request_proc].call(request) if options[:request_proc].respond_to?(:call)
190
183
  validate_request(request)
191
184
 
192
- http_options = options[:http_options] || {}
193
- http_options[:use_ssl] = (uri.scheme == 'https')
185
+ http_options = (options[:http_options] || {}).merge(
186
+ use_ssl: uri.scheme == 'https',
187
+ ipaddr: ip
188
+ )
194
189
 
195
- with_forced_hostname(hostname) do
196
- ::Net::HTTP.start(uri.hostname, uri.port, **http_options) do |http|
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
207
- end
208
- return response, url
190
+ ::Net::HTTP.start(uri.hostname, uri.port, **http_options) do |http|
191
+ response = http.request(request) do |res|
192
+ block&.call(res)
193
+ end
194
+ case response
195
+ when ::Net::HTTPRedirection
196
+ url = response['location']
197
+ # Handle relative redirects
198
+ url = "#{uri.scheme}://#{normalized_hostname(uri)}#{url}" if url.start_with?('/')
199
+ else
200
+ url = nil
209
201
  end
202
+ return response, url
210
203
  end
211
204
  end
212
205
  private_class_method :fetch_once
@@ -223,12 +216,4 @@ class SsrfFilter
223
216
  end
224
217
  end
225
218
  private_class_method :validate_request
226
-
227
- def self.with_forced_hostname(hostname, &_block)
228
- ::Thread.current[FIBER_HOSTNAME_KEY] = hostname
229
- yield
230
- ensure
231
- ::Thread.current[FIBER_HOSTNAME_KEY] = nil
232
- end
233
- private_class_method :with_forced_hostname
234
219
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class SsrfFilter
4
- VERSION = '1.1.2'
4
+ VERSION = '1.2.0'
5
5
  end
data/lib/ssrf_filter.rb CHANGED
@@ -1,5 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'ssrf_filter/patch/ssl_socket'
4
3
  require 'ssrf_filter/ssrf_filter'
5
4
  require 'ssrf_filter/version'
metadata CHANGED
@@ -1,29 +1,43 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ssrf_filter
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.2
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Arkadiy Tetelman
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-09-01 00:00:00.000000000 Z
11
+ date: 2024-11-08 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: base64
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 0.2.0
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 0.2.0
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: bundler-audit
15
29
  requirement: !ruby/object:Gem::Requirement
16
30
  requirements:
17
31
  - - "~>"
18
32
  - !ruby/object:Gem::Version
19
- version: 0.9.1
33
+ version: 0.9.2
20
34
  type: :development
21
35
  prerelease: false
22
36
  version_requirements: !ruby/object:Gem::Requirement
23
37
  requirements:
24
38
  - - "~>"
25
39
  - !ruby/object:Gem::Version
26
- version: 0.9.1
40
+ version: 0.9.2
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: pry-byebug
29
43
  requirement: !ruby/object:Gem::Requirement
@@ -44,42 +58,42 @@ dependencies:
44
58
  requirements:
45
59
  - - "~>"
46
60
  - !ruby/object:Gem::Version
47
- version: 3.12.0
61
+ version: 3.13.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.12.0
68
+ version: 3.13.0
55
69
  - !ruby/object:Gem::Dependency
56
70
  name: rubocop
57
71
  requirement: !ruby/object:Gem::Requirement
58
72
  requirements:
59
73
  - - "~>"
60
74
  - !ruby/object:Gem::Version
61
- version: 1.35.0
75
+ version: 1.68.0
62
76
  type: :development
63
77
  prerelease: false
64
78
  version_requirements: !ruby/object:Gem::Requirement
65
79
  requirements:
66
80
  - - "~>"
67
81
  - !ruby/object:Gem::Version
68
- version: 1.35.0
82
+ version: 1.68.0
69
83
  - !ruby/object:Gem::Dependency
70
84
  name: rubocop-rspec
71
85
  requirement: !ruby/object:Gem::Requirement
72
86
  requirements:
73
87
  - - "~>"
74
88
  - !ruby/object:Gem::Version
75
- version: 2.12.1
89
+ version: 3.2.0
76
90
  type: :development
77
91
  prerelease: false
78
92
  version_requirements: !ruby/object:Gem::Requirement
79
93
  requirements:
80
94
  - - "~>"
81
95
  - !ruby/object:Gem::Version
82
- version: 2.12.1
96
+ version: 3.2.0
83
97
  - !ruby/object:Gem::Dependency
84
98
  name: simplecov
85
99
  requirement: !ruby/object:Gem::Requirement
@@ -114,14 +128,14 @@ dependencies:
114
128
  requirements:
115
129
  - - ">="
116
130
  - !ruby/object:Gem::Version
117
- version: 3.18.0
131
+ version: 3.24.0
118
132
  type: :development
119
133
  prerelease: false
120
134
  version_requirements: !ruby/object:Gem::Requirement
121
135
  requirements:
122
136
  - - ">="
123
137
  - !ruby/object:Gem::Version
124
- version: 3.18.0
138
+ version: 3.24.0
125
139
  - !ruby/object:Gem::Dependency
126
140
  name: webrick
127
141
  requirement: !ruby/object:Gem::Requirement
@@ -144,7 +158,6 @@ extensions: []
144
158
  extra_rdoc_files: []
145
159
  files:
146
160
  - lib/ssrf_filter.rb
147
- - lib/ssrf_filter/patch/ssl_socket.rb
148
161
  - lib/ssrf_filter/ssrf_filter.rb
149
162
  - lib/ssrf_filter/version.rb
150
163
  homepage: https://github.com/arkadiyt/ssrf_filter
@@ -160,7 +173,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
160
173
  requirements:
161
174
  - - ">="
162
175
  - !ruby/object:Gem::Version
163
- version: 2.6.0
176
+ version: 2.7.0
164
177
  required_rubygems_version: !ruby/object:Gem::Requirement
165
178
  requirements:
166
179
  - - ">="
@@ -1,66 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class SsrfFilter
4
- module Patch
5
- module SSLSocket
6
- def self.apply!
7
- return if instance_variable_defined?(:@patched_ssl_socket)
8
-
9
- @patched_ssl_socket = true
10
-
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
- original_post_connection_check = instance_method(:post_connection_check)
37
- define_method(:post_connection_check) do |hostname|
38
- original_post_connection_check.bind(self).call(::Thread.current[::SsrfFilter::FIBER_HOSTNAME_KEY] ||
39
- hostname)
40
- end
41
-
42
- if method_defined?(:hostname=)
43
- original_hostname = instance_method(:hostname=)
44
- define_method(:hostname=) do |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]
60
- end
61
- end
62
- end
63
- end
64
- end
65
- end
66
- end