ssrf_filter 1.0.7 → 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: 17984597a9ad2c3dd852793334951478090086e5c6b7f86d67cf827fa98bff4c
4
- data.tar.gz: 99c09e926f2b2c3a040dd0deed245ebb005f6cb242a185a7bca15592183f0b82
3
+ metadata.gz: f0230e20c7cd24b24dad277ee01042f3a4885fa68a8ea593fe0607de92023026
4
+ data.tar.gz: 6ce71a83907b1acfc382acc5d859a26fc1471dc3cd2dbc48d14b3e1dfca12051
5
5
  SHA512:
6
- metadata.gz: ad60b8c2efaca1de0b7d89de0102eef87d23c65d1c6b73e674092e7740e8c5fe1a006801c648529bf9150eb590df9b3c160af519b666176f41697ef69218918e
7
- data.tar.gz: '062843311bfdb75b9de842fb29e4864da4a4acc4c41d454fec593ee3c0a86f306cddd30d4cc9c46b6d6b169b2e3e3480c2e8499cb637eaf97d96afa534b41569'
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
@@ -76,10 +78,13 @@ class SsrfFilter
76
78
  get: ::Net::HTTP::Get,
77
79
  put: ::Net::HTTP::Put,
78
80
  post: ::Net::HTTP::Post,
79
- delete: ::Net::HTTP::Delete
81
+ delete: ::Net::HTTP::Delete,
82
+ head: ::Net::HTTP::Head,
83
+ patch: ::Net::HTTP::Patch
80
84
  }.freeze
81
85
 
82
- FIBER_LOCAL_KEY = :__ssrf_filter_hostname
86
+ FIBER_HOSTNAME_KEY = :__ssrf_filter_hostname
87
+ FIBER_ADDRESS_KEY = :__ssrf_filter_address
83
88
 
84
89
  class Error < ::StandardError
85
90
  end
@@ -99,10 +104,10 @@ 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!
110
+ ::SsrfFilter::Patch::Resolv.apply!
106
111
 
107
112
  original_url = url
108
113
  scheme_whitelist = options[:scheme_whitelist] || DEFAULT_SCHEME_WHITELIST
@@ -124,16 +129,8 @@ class SsrfFilter
124
129
  public_addresses = ip_addresses.reject(&method(:unsafe_ip_address?))
125
130
  raise PrivateIPAddress, "Hostname '#{hostname}' has no public ip addresses" if public_addresses.empty?
126
131
 
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
132
+ response, url = fetch_once(uri, public_addresses.sample.to_s, method, options, &block)
133
+ return response if url.nil?
137
134
  end
138
135
 
139
136
  raise TooManyRedirects, "Got #{max_redirects} redirects fetching #{original_url}"
@@ -169,7 +166,7 @@ class SsrfFilter
169
166
 
170
167
  def self.fetch_once(uri, ip, verb, options, &block)
171
168
  if options[:params]
172
- params = uri.query ? ::Hash[::URI.decode_www_form(uri.query)] : {}
169
+ params = uri.query ? ::URI.decode_www_form(uri.query).to_h : {}
173
170
  params.merge!(options[:params])
174
171
  uri.query = ::URI.encode_www_form(params)
175
172
  end
@@ -186,15 +183,26 @@ class SsrfFilter
186
183
 
187
184
  request.body = options[:body] if options[:body]
188
185
 
189
- block.call(request) if block_given?
186
+ options[:request_proc].call(request) if options[:request_proc].respond_to?(:call)
190
187
  validate_request(request)
191
188
 
192
189
  http_options = options[:http_options] || {}
193
190
  http_options[:use_ssl] = (uri.scheme == 'https')
194
191
 
195
- with_forced_hostname(hostname) do
196
- ::Net::HTTP.start(uri.hostname, uri.port, http_options) do |http|
197
- 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
198
206
  end
199
207
  end
200
208
  end
@@ -213,11 +221,13 @@ class SsrfFilter
213
221
  end
214
222
  private_class_method :validate_request
215
223
 
216
- def self.with_forced_hostname(hostname, &_block)
217
- ::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
218
227
  yield
219
228
  ensure
220
- ::Thread.current[FIBER_LOCAL_KEY] = nil
229
+ ::Thread.current[FIBER_HOSTNAME_KEY] = nil
230
+ ::Thread.current[FIBER_ADDRESS_KEY] = nil
221
231
  end
222
232
  private_class_method :with_forced_hostname
223
233
  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.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.7
4
+ version: 1.1.1
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: 2022-09-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler-audit
@@ -16,87 +16,144 @@ 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.11.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.11.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.21.2
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: 0.21.2
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
147
+ - lib/ssrf_filter/patch/resolv.rb
92
148
  - lib/ssrf_filter/patch/ssl_socket.rb
93
149
  - lib/ssrf_filter/ssrf_filter.rb
94
150
  - lib/ssrf_filter/version.rb
95
151
  homepage: https://github.com/arkadiyt/ssrf_filter
96
152
  licenses:
97
153
  - MIT
98
- metadata: {}
99
- post_install_message:
154
+ metadata:
155
+ rubygems_mfa_required: 'true'
156
+ post_install_message:
100
157
  rdoc_options: []
101
158
  require_paths:
102
159
  - lib
@@ -104,7 +161,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
104
161
  requirements:
105
162
  - - ">="
106
163
  - !ruby/object:Gem::Version
107
- version: 2.0.0
164
+ version: 2.6.0
108
165
  required_rubygems_version: !ruby/object:Gem::Requirement
109
166
  requirements:
110
167
  - - ">="
@@ -112,7 +169,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
112
169
  version: '0'
113
170
  requirements: []
114
171
  rubygems_version: 3.0.3
115
- signing_key:
172
+ signing_key:
116
173
  specification_version: 4
117
174
  summary: A gem that makes it easy to prevent server side request forgery (SSRF) attacks
118
175
  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