ssrf_filter 1.0.8 → 1.1.1

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: 3ad08c7683abaf9c1cce31358e91eebd54e2276911da99c3d7bdbc1481ec6a8f
4
- data.tar.gz: 4993290726f69399264c18dfe8b4ef1c908b78c726f0cfb5dcbc5b9c09043ead
3
+ metadata.gz: f0230e20c7cd24b24dad277ee01042f3a4885fa68a8ea593fe0607de92023026
4
+ data.tar.gz: 6ce71a83907b1acfc382acc5d859a26fc1471dc3cd2dbc48d14b3e1dfca12051
5
5
  SHA512:
6
- metadata.gz: 522368253a813feb3ac735f419b8d9fe6ac3b006debf3e3c003f6909d809b5b51af06ccc9c42b7cd8d18e90d54a298d3a39fee0514304a1c3a2a1ffd1d54580d
7
- data.tar.gz: 21cd1fe841b77536db7e51e9cddc94b7ed96305bf766bb16df77e5b83029a6b6c44e042d1036ae52a135419e72ae70ec1155c471fdc6596eb9dab07809e54e36
6
+ metadata.gz: cb56468fb4bdcf10168efbc18f004d996363b9dcc8877a1fab1eff508ef1519b86c7eeb750fedd8b565a162804746f9a036b54e584520137c1c2b1a86d5c9421
7
+ data.tar.gz: 9d5e0ea62956d551caa2474a384fa90e827c6d133bbfe04831f77dd49cabe5bcb3abc7008eefc63bccedf1634a33bcc27f8178253ec26f7e051075dcfba7066e
@@ -0,0 +1,44 @@
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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class SsrfFilter
2
4
  module Patch
3
5
  module SSLSocket
@@ -33,13 +35,14 @@ class SsrfFilter
33
35
  ::OpenSSL::SSL::SSLSocket.class_eval do
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)
43
46
  end
44
47
  end
45
48
  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
@@ -81,7 +83,8 @@ class SsrfFilter
81
83
  patch: ::Net::HTTP::Patch
82
84
  }.freeze
83
85
 
84
- FIBER_LOCAL_KEY = :__ssrf_filter_hostname
86
+ FIBER_HOSTNAME_KEY = :__ssrf_filter_hostname
87
+ FIBER_ADDRESS_KEY = :__ssrf_filter_address
85
88
 
86
89
  class Error < ::StandardError
87
90
  end
@@ -104,7 +107,7 @@ class SsrfFilter
104
107
  %i[get put post delete head patch].each do |method|
105
108
  define_singleton_method(method) do |url, options = {}, &block|
106
109
  ::SsrfFilter::Patch::SSLSocket.apply!
107
- ::SsrfFilter::Patch::HTTPGenericRequest.apply!
110
+ ::SsrfFilter::Patch::Resolv.apply!
108
111
 
109
112
  original_url = url
110
113
  scheme_whitelist = options[:scheme_whitelist] || DEFAULT_SCHEME_WHITELIST
@@ -126,16 +129,8 @@ class SsrfFilter
126
129
  public_addresses = ip_addresses.reject(&method(:unsafe_ip_address?))
127
130
  raise PrivateIPAddress, "Hostname '#{hostname}' has no public ip addresses" if public_addresses.empty?
128
131
 
129
- response = fetch_once(uri, public_addresses.sample.to_s, method, options, &block)
130
-
131
- case response
132
- when ::Net::HTTPRedirection then
133
- url = response['location']
134
- # Handle relative redirects
135
- url = "#{uri.scheme}://#{hostname}:#{uri.port}#{url}" if url.start_with?('/')
136
- else
137
- return response
138
- end
132
+ response, url = fetch_once(uri, public_addresses.sample.to_s, method, options, &block)
133
+ return response if url.nil?
139
134
  end
140
135
 
141
136
  raise TooManyRedirects, "Got #{max_redirects} redirects fetching #{original_url}"
@@ -171,7 +166,7 @@ class SsrfFilter
171
166
 
172
167
  def self.fetch_once(uri, ip, verb, options, &block)
173
168
  if options[:params]
174
- params = uri.query ? ::Hash[::URI.decode_www_form(uri.query)] : {}
169
+ params = uri.query ? ::URI.decode_www_form(uri.query).to_h : {}
175
170
  params.merge!(options[:params])
176
171
  uri.query = ::URI.encode_www_form(params)
177
172
  end
@@ -188,15 +183,26 @@ class SsrfFilter
188
183
 
189
184
  request.body = options[:body] if options[:body]
190
185
 
191
- block.call(request) if block_given?
186
+ options[:request_proc].call(request) if options[:request_proc].respond_to?(:call)
192
187
  validate_request(request)
193
188
 
194
189
  http_options = options[:http_options] || {}
195
190
  http_options[:use_ssl] = (uri.scheme == 'https')
196
191
 
197
- with_forced_hostname(hostname) do
198
- ::Net::HTTP.start(uri.hostname, uri.port, http_options) do |http|
199
- http.request(request)
192
+ with_forced_hostname(hostname, ip) do
193
+ ::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
205
+ end
200
206
  end
201
207
  end
202
208
  end
@@ -215,11 +221,13 @@ class SsrfFilter
215
221
  end
216
222
  private_class_method :validate_request
217
223
 
218
- def self.with_forced_hostname(hostname, &_block)
219
- ::Thread.current[FIBER_LOCAL_KEY] = hostname
224
+ def self.with_forced_hostname(hostname, ip, &_block)
225
+ ::Thread.current[FIBER_HOSTNAME_KEY] = hostname
226
+ ::Thread.current[FIBER_ADDRESS_KEY] = ip
220
227
  yield
221
228
  ensure
222
- ::Thread.current[FIBER_LOCAL_KEY] = nil
229
+ ::Thread.current[FIBER_HOSTNAME_KEY] = nil
230
+ ::Thread.current[FIBER_ADDRESS_KEY] = nil
223
231
  end
224
232
  private_class_method :with_forced_hostname
225
233
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class SsrfFilter
4
- VERSION = '1.0.8'.freeze
4
+ VERSION = '1.1.1'
5
5
  end
data/lib/ssrf_filter.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'ssrf_filter/patch/http_generic_request'
3
+ require 'ssrf_filter/patch/resolv'
4
4
  require 'ssrf_filter/patch/ssl_socket'
5
5
  require 'ssrf_filter/ssrf_filter'
6
6
  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.8
4
+ version: 1.1.1
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-04 00:00:00.000000000 Z
11
+ date: 2022-09-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler-audit
@@ -16,126 +16,126 @@ 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
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - "~>"
31
+ - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: 0.8.22
33
+ version: '0'
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - "~>"
38
+ - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: 0.8.22
40
+ version: '0'
41
41
  - !ruby/object:Gem::Dependency
42
- name: psych
42
+ name: rspec
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
- - - "<"
45
+ - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: '4'
47
+ version: 3.11.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: '4'
54
+ version: 3.11.0
55
55
  - !ruby/object:Gem::Dependency
56
- name: rspec
56
+ name: rubocop
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
59
  - - "~>"
60
60
  - !ruby/object:Gem::Version
61
- version: 3.8.0
61
+ version: 1.35.0
62
62
  type: :development
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
- version: 3.8.0
68
+ version: 1.35.0
69
69
  - !ruby/object:Gem::Dependency
70
- name: webmock
70
+ name: rubocop-rspec
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
- - - ">="
73
+ - - "~>"
74
74
  - !ruby/object:Gem::Version
75
- version: 3.5.1
75
+ version: 2.12.1
76
76
  type: :development
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
- - - ">="
80
+ - - "~>"
81
81
  - !ruby/object:Gem::Version
82
- version: 3.5.1
82
+ version: 2.12.1
83
83
  - !ruby/object:Gem::Dependency
84
- name: webrick
84
+ name: simplecov
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
- - - ">="
87
+ - - "~>"
88
88
  - !ruby/object:Gem::Version
89
- version: '0'
89
+ version: 0.21.2
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'
96
+ version: 0.21.2
97
97
  - !ruby/object:Gem::Dependency
98
- name: public_suffix
98
+ name: simplecov-lcov
99
99
  requirement: !ruby/object:Gem::Requirement
100
100
  requirements:
101
- - - '='
101
+ - - "~>"
102
102
  - !ruby/object:Gem::Version
103
- version: 2.0.5
103
+ version: 0.8.0
104
104
  type: :development
105
105
  prerelease: false
106
106
  version_requirements: !ruby/object:Gem::Requirement
107
107
  requirements:
108
- - - '='
108
+ - - "~>"
109
109
  - !ruby/object:Gem::Version
110
- version: 2.0.5
110
+ version: 0.8.0
111
111
  - !ruby/object:Gem::Dependency
112
- name: rexml
112
+ name: webmock
113
113
  requirement: !ruby/object:Gem::Requirement
114
114
  requirements:
115
- - - '='
115
+ - - ">="
116
116
  - !ruby/object:Gem::Version
117
- version: 3.2.4
117
+ version: 3.18.0
118
118
  type: :development
119
119
  prerelease: false
120
120
  version_requirements: !ruby/object:Gem::Requirement
121
121
  requirements:
122
- - - '='
122
+ - - ">="
123
123
  - !ruby/object:Gem::Version
124
- version: 3.2.4
124
+ version: 3.18.0
125
125
  - !ruby/object:Gem::Dependency
126
- name: rubocop
126
+ name: webrick
127
127
  requirement: !ruby/object:Gem::Requirement
128
128
  requirements:
129
- - - "~>"
129
+ - - ">="
130
130
  - !ruby/object:Gem::Version
131
- version: 0.50.0
131
+ version: '0'
132
132
  type: :development
133
133
  prerelease: false
134
134
  version_requirements: !ruby/object:Gem::Requirement
135
135
  requirements:
136
- - - "~>"
136
+ - - ">="
137
137
  - !ruby/object:Gem::Version
138
- version: 0.50.0
138
+ version: '0'
139
139
  description: A gem that makes it easy to prevent server side request forgery (SSRF)
140
140
  attacks
141
141
  email:
@@ -144,14 +144,15 @@ extensions: []
144
144
  extra_rdoc_files: []
145
145
  files:
146
146
  - lib/ssrf_filter.rb
147
- - lib/ssrf_filter/patch/http_generic_request.rb
147
+ - lib/ssrf_filter/patch/resolv.rb
148
148
  - lib/ssrf_filter/patch/ssl_socket.rb
149
149
  - lib/ssrf_filter/ssrf_filter.rb
150
150
  - lib/ssrf_filter/version.rb
151
151
  homepage: https://github.com/arkadiyt/ssrf_filter
152
152
  licenses:
153
153
  - MIT
154
- metadata: {}
154
+ metadata:
155
+ rubygems_mfa_required: 'true'
155
156
  post_install_message:
156
157
  rdoc_options: []
157
158
  require_paths:
@@ -160,14 +161,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
160
161
  requirements:
161
162
  - - ">="
162
163
  - !ruby/object:Gem::Version
163
- version: 2.0.0
164
+ version: 2.6.0
164
165
  required_rubygems_version: !ruby/object:Gem::Requirement
165
166
  requirements:
166
167
  - - ">="
167
168
  - !ruby/object:Gem::Version
168
169
  version: '0'
169
170
  requirements: []
170
- rubygems_version: 3.2.15
171
+ rubygems_version: 3.0.3
171
172
  signing_key:
172
173
  specification_version: 4
173
174
  summary: A gem that makes it easy to prevent server side request forgery (SSRF) attacks
@@ -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