ssrf_filter 1.0.7 → 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: 17984597a9ad2c3dd852793334951478090086e5c6b7f86d67cf827fa98bff4c
4
- data.tar.gz: 99c09e926f2b2c3a040dd0deed245ebb005f6cb242a185a7bca15592183f0b82
3
+ metadata.gz: 315ab29498751c0bb60964f06932464d8663ec66e50ed403b04c2e19c9fce5e6
4
+ data.tar.gz: 8b3475337b149bf8e04dd11e379e9c0698f74d05528524fea847ed996aff405c
5
5
  SHA512:
6
- metadata.gz: ad60b8c2efaca1de0b7d89de0102eef87d23c65d1c6b73e674092e7740e8c5fe1a006801c648529bf9150eb590df9b3c160af519b666176f41697ef69218918e
7
- data.tar.gz: '062843311bfdb75b9de842fb29e4864da4a4acc4c41d454fec593ee3c0a86f306cddd30d4cc9c46b6d6b169b2e3e3480c2e8499cb637eaf97d96afa534b41569'
6
+ metadata.gz: 299b91d225b906faa91a1ebd4a8ad32ce46889deaeffa89d5a7488b871310e68ed7f73332c9ee25fd0ff3d01a25dd949907a390e8ce44926910657e2fa054f3e
7
+ data.tar.gz: 89b6051cb2a8f232336e1e25e95a5898c71ba4439f582224452effbcc41b3a68af13d1e16d855f8ebb7634c013fefd3301c661d5d6bf72f3d98894feb867f47f
@@ -1,45 +1,62 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class SsrfFilter
2
4
  module Patch
3
5
  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
6
  def self.apply!
29
7
  return if instance_variable_defined?(:@patched_ssl_socket)
30
8
 
31
9
  @patched_ssl_socket = true
32
10
 
33
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
+
34
36
  original_post_connection_check = instance_method(:post_connection_check)
35
37
  define_method(:post_connection_check) do |hostname|
36
- 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)
37
40
  end
38
41
 
39
42
  if method_defined?(:hostname=)
40
43
  original_hostname = instance_method(:hostname=)
41
44
  define_method(:hostname=) do |hostname|
42
- 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]
43
60
  end
44
61
  end
45
62
  end
@@ -10,7 +10,9 @@ 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
- mask_addr >>= 1 while (mask_addr & 0x1).zero?
13
+ while (mask_addr & 0x1).zero?
14
+ mask_addr >>= 1
15
+ end
14
16
 
15
17
  length = 0
16
18
  while mask_addr & 0x1 == 0x1
@@ -70,16 +72,19 @@ class SsrfFilter
70
72
  ::Resolv.getaddresses(hostname).map { |ip| ::IPAddr.new(ip) }
71
73
  end
72
74
 
75
+ DEFAULT_ALLOW_UNFOLLOWED_REDIRECTS = false
73
76
  DEFAULT_MAX_REDIRECTS = 10
74
77
 
75
78
  VERB_MAP = {
76
79
  get: ::Net::HTTP::Get,
77
80
  put: ::Net::HTTP::Put,
78
81
  post: ::Net::HTTP::Post,
79
- delete: ::Net::HTTP::Delete
82
+ delete: ::Net::HTTP::Delete,
83
+ head: ::Net::HTTP::Head,
84
+ patch: ::Net::HTTP::Patch
80
85
  }.freeze
81
86
 
82
- FIBER_LOCAL_KEY = :__ssrf_filter_hostname
87
+ FIBER_HOSTNAME_KEY = :__ssrf_filter_hostname
83
88
 
84
89
  class Error < ::StandardError
85
90
  end
@@ -99,17 +104,18 @@ class SsrfFilter
99
104
  class CRLFInjection < Error
100
105
  end
101
106
 
102
- %i[get put post delete].each do |method|
107
+ %i[get put post delete head patch].each do |method|
103
108
  define_singleton_method(method) do |url, options = {}, &block|
104
109
  ::SsrfFilter::Patch::SSLSocket.apply!
105
- ::SsrfFilter::Patch::HTTPGenericRequest.apply!
106
110
 
107
111
  original_url = url
108
- scheme_whitelist = options[:scheme_whitelist] || DEFAULT_SCHEME_WHITELIST
109
- resolver = options[:resolver] || DEFAULT_RESOLVER
110
- 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)
111
116
  url = url.to_s
112
117
 
118
+ response = nil
113
119
  (max_redirects + 1).times do
114
120
  uri = URI(url)
115
121
 
@@ -124,18 +130,12 @@ class SsrfFilter
124
130
  public_addresses = ip_addresses.reject(&method(:unsafe_ip_address?))
125
131
  raise PrivateIPAddress, "Hostname '#{hostname}' has no public ip addresses" if public_addresses.empty?
126
132
 
127
- response = fetch_once(uri, public_addresses.sample.to_s, method, options, &block)
128
-
129
- case response
130
- when ::Net::HTTPRedirection then
131
- url = response['location']
132
- # Handle relative redirects
133
- url = "#{uri.scheme}://#{hostname}:#{uri.port}#{url}" if url.start_with?('/')
134
- else
135
- return response
136
- end
133
+ response, url = fetch_once(uri, public_addresses.sample.to_s, method, options, &block)
134
+ return response if url.nil?
137
135
  end
138
136
 
137
+ return response if allow_unfollowed_redirects
138
+
139
139
  raise TooManyRedirects, "Got #{max_redirects} redirects fetching #{original_url}"
140
140
  end
141
141
  end
@@ -169,7 +169,7 @@ class SsrfFilter
169
169
 
170
170
  def self.fetch_once(uri, ip, verb, options, &block)
171
171
  if options[:params]
172
- params = uri.query ? ::Hash[::URI.decode_www_form(uri.query)] : {}
172
+ params = uri.query ? ::URI.decode_www_form(uri.query).to_h : {}
173
173
  params.merge!(options[:params])
174
174
  uri.query = ::URI.encode_www_form(params)
175
175
  end
@@ -186,15 +186,26 @@ class SsrfFilter
186
186
 
187
187
  request.body = options[:body] if options[:body]
188
188
 
189
- block.call(request) if block_given?
189
+ options[:request_proc].call(request) if options[:request_proc].respond_to?(:call)
190
190
  validate_request(request)
191
191
 
192
192
  http_options = options[:http_options] || {}
193
193
  http_options[:use_ssl] = (uri.scheme == 'https')
194
194
 
195
195
  with_forced_hostname(hostname) do
196
- ::Net::HTTP.start(uri.hostname, uri.port, http_options) do |http|
197
- http.request(request)
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
198
209
  end
199
210
  end
200
211
  end
@@ -214,10 +225,10 @@ class SsrfFilter
214
225
  private_class_method :validate_request
215
226
 
216
227
  def self.with_forced_hostname(hostname, &_block)
217
- ::Thread.current[FIBER_LOCAL_KEY] = hostname
228
+ ::Thread.current[FIBER_HOSTNAME_KEY] = hostname
218
229
  yield
219
230
  ensure
220
- ::Thread.current[FIBER_LOCAL_KEY] = nil
231
+ ::Thread.current[FIBER_HOSTNAME_KEY] = nil
221
232
  end
222
233
  private_class_method :with_forced_hostname
223
234
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class SsrfFilter
4
- VERSION = '1.0.7'.freeze
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/http_generic_request'
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.0.7
4
+ version: 1.1.2
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: 2019-10-21 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
@@ -16,87 +16,143 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: 0.6.1
19
+ version: 0.9.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.1
26
+ version: 0.9.1
27
27
  - !ruby/object:Gem::Dependency
28
- name: coveralls
28
+ name: pry-byebug
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
29
43
  requirement: !ruby/object:Gem::Requirement
30
44
  requirements:
31
45
  - - "~>"
32
46
  - !ruby/object:Gem::Version
33
- version: 0.8.22
47
+ version: 3.12.0
34
48
  type: :development
35
49
  prerelease: false
36
50
  version_requirements: !ruby/object:Gem::Requirement
37
51
  requirements:
38
52
  - - "~>"
39
53
  - !ruby/object:Gem::Version
40
- version: 0.8.22
54
+ version: 3.12.0
41
55
  - !ruby/object:Gem::Dependency
42
- name: rspec
56
+ name: rubocop
43
57
  requirement: !ruby/object:Gem::Requirement
44
58
  requirements:
45
59
  - - "~>"
46
60
  - !ruby/object:Gem::Version
47
- version: 3.8.0
61
+ version: 1.35.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.8.0
68
+ version: 1.35.0
55
69
  - !ruby/object:Gem::Dependency
56
- name: webmock
70
+ name: rubocop-rspec
57
71
  requirement: !ruby/object:Gem::Requirement
58
72
  requirements:
59
73
  - - "~>"
60
74
  - !ruby/object:Gem::Version
61
- version: 3.5.1
75
+ version: 2.12.1
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: 3.5.1
82
+ version: 2.12.1
69
83
  - !ruby/object:Gem::Dependency
70
- name: rubocop
84
+ name: simplecov
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: 0.22.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.22.0
97
+ - !ruby/object:Gem::Dependency
98
+ name: simplecov-lcov
71
99
  requirement: !ruby/object:Gem::Requirement
72
100
  requirements:
73
101
  - - "~>"
74
102
  - !ruby/object:Gem::Version
75
- version: 0.65.0
103
+ version: 0.8.0
76
104
  type: :development
77
105
  prerelease: false
78
106
  version_requirements: !ruby/object:Gem::Requirement
79
107
  requirements:
80
108
  - - "~>"
81
109
  - !ruby/object:Gem::Version
82
- version: 0.65.0
110
+ version: 0.8.0
111
+ - !ruby/object:Gem::Dependency
112
+ name: webmock
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: 3.18.0
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: 3.18.0
125
+ - !ruby/object:Gem::Dependency
126
+ name: webrick
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '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
91
- - lib/ssrf_filter/patch/http_generic_request.rb
92
147
  - lib/ssrf_filter/patch/ssl_socket.rb
93
148
  - lib/ssrf_filter/ssrf_filter.rb
94
149
  - lib/ssrf_filter/version.rb
95
150
  homepage: https://github.com/arkadiyt/ssrf_filter
96
151
  licenses:
97
152
  - MIT
98
- metadata: {}
99
- post_install_message:
153
+ metadata:
154
+ rubygems_mfa_required: 'true'
155
+ post_install_message:
100
156
  rdoc_options: []
101
157
  require_paths:
102
158
  - lib
@@ -104,15 +160,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
104
160
  requirements:
105
161
  - - ">="
106
162
  - !ruby/object:Gem::Version
107
- version: 2.0.0
163
+ version: 2.6.0
108
164
  required_rubygems_version: !ruby/object:Gem::Requirement
109
165
  requirements:
110
166
  - - ">="
111
167
  - !ruby/object:Gem::Version
112
168
  version: '0'
113
169
  requirements: []
114
- rubygems_version: 3.0.3
115
- signing_key:
170
+ rubygems_version: 3.3.7
171
+ signing_key:
116
172
  specification_version: 4
117
173
  summary: A gem that makes it easy to prevent server side request forgery (SSRF) attacks
118
174
  test_files: []
@@ -1,83 +0,0 @@
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