ssrf_filter 1.1.1 → 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: f0230e20c7cd24b24dad277ee01042f3a4885fa68a8ea593fe0607de92023026
4
- data.tar.gz: 6ce71a83907b1acfc382acc5d859a26fc1471dc3cd2dbc48d14b3e1dfca12051
3
+ metadata.gz: c7b6cb5db50edebd286cc411415c02c7e461815da7e21b75d5cc2d0780b149cb
4
+ data.tar.gz: e48564883db3e5db9b7e0d54a134680fe035f8f33f8f74a378dc75448545347a
5
5
  SHA512:
6
- metadata.gz: cb56468fb4bdcf10168efbc18f004d996363b9dcc8877a1fab1eff508ef1519b86c7eeb750fedd8b565a162804746f9a036b54e584520137c1c2b1a86d5c9421
7
- data.tar.gz: 9d5e0ea62956d551caa2474a384fa90e827c6d133bbfe04831f77dd49cabe5bcb3abc7008eefc63bccedf1634a33bcc27f8178253ec26f7e051075dcfba7066e
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
 
@@ -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,9 +84,6 @@ class SsrfFilter
83
84
  patch: ::Net::HTTP::Patch
84
85
  }.freeze
85
86
 
86
- FIBER_HOSTNAME_KEY = :__ssrf_filter_hostname
87
- FIBER_ADDRESS_KEY = :__ssrf_filter_address
88
-
89
87
  class Error < ::StandardError
90
88
  end
91
89
 
@@ -106,15 +104,14 @@ 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
- ::SsrfFilter::Patch::Resolv.apply!
111
-
112
107
  original_url = url
113
- scheme_whitelist = options[:scheme_whitelist] || DEFAULT_SCHEME_WHITELIST
114
- resolver = options[:resolver] || DEFAULT_RESOLVER
115
- max_redirects = options[:max_redirects] || DEFAULT_MAX_REDIRECTS
108
+ scheme_whitelist = options.fetch(:scheme_whitelist, DEFAULT_SCHEME_WHITELIST)
109
+ resolver = options.fetch(:resolver, DEFAULT_RESOLVER)
110
+ allow_unfollowed_redirects = options.fetch(:allow_unfollowed_redirects, DEFAULT_ALLOW_UNFOLLOWED_REDIRECTS)
111
+ max_redirects = options.fetch(:max_redirects, DEFAULT_MAX_REDIRECTS)
116
112
  url = url.to_s
117
113
 
114
+ response = nil
118
115
  (max_redirects + 1).times do
119
116
  uri = URI(url)
120
117
 
@@ -133,6 +130,8 @@ class SsrfFilter
133
130
  return response if url.nil?
134
131
  end
135
132
 
133
+ return response if allow_unfollowed_redirects
134
+
136
135
  raise TooManyRedirects, "Got #{max_redirects} redirects fetching #{original_url}"
137
136
  end
138
137
  end
@@ -153,16 +152,16 @@ class SsrfFilter
153
152
  end
154
153
  private_class_method :ipaddr_has_mask?
155
154
 
156
- def self.host_header(hostname, uri)
155
+ def self.normalized_hostname(uri)
157
156
  # Attach port for non-default as per RFC2616
158
157
  if (uri.port == 80 && uri.scheme == 'http') ||
159
158
  (uri.port == 443 && uri.scheme == 'https')
160
- hostname
159
+ uri.hostname
161
160
  else
162
- "#{hostname}:#{uri.port}"
161
+ "#{uri.hostname}:#{uri.port}"
163
162
  end
164
163
  end
165
- private_class_method :host_header
164
+ private_class_method :normalized_hostname
166
165
 
167
166
  def self.fetch_once(uri, ip, verb, options, &block)
168
167
  if options[:params]
@@ -171,11 +170,8 @@ class SsrfFilter
171
170
  uri.query = ::URI.encode_www_form(params)
172
171
  end
173
172
 
174
- hostname = uri.hostname
175
- uri.hostname = ip
176
-
177
173
  request = VERB_MAP[verb].new(uri)
178
- request['host'] = host_header(hostname, uri)
174
+ request['host'] = normalized_hostname(uri)
179
175
 
180
176
  Array(options[:headers]).each do |header, value|
181
177
  request[header] = value
@@ -186,24 +182,24 @@ class SsrfFilter
186
182
  options[:request_proc].call(request) if options[:request_proc].respond_to?(:call)
187
183
  validate_request(request)
188
184
 
189
- http_options = options[:http_options] || {}
190
- http_options[:use_ssl] = (uri.scheme == 'https')
191
-
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
185
+ http_options = (options[:http_options] || {}).merge(
186
+ use_ssl: uri.scheme == 'https',
187
+ ipaddr: ip
188
+ )
189
+
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
206
201
  end
202
+ return response, url
207
203
  end
208
204
  end
209
205
  private_class_method :fetch_once
@@ -220,14 +216,4 @@ class SsrfFilter
220
216
  end
221
217
  end
222
218
  private_class_method :validate_request
223
-
224
- def self.with_forced_hostname(hostname, ip, &_block)
225
- ::Thread.current[FIBER_HOSTNAME_KEY] = hostname
226
- ::Thread.current[FIBER_ADDRESS_KEY] = ip
227
- yield
228
- ensure
229
- ::Thread.current[FIBER_HOSTNAME_KEY] = nil
230
- ::Thread.current[FIBER_ADDRESS_KEY] = nil
231
- end
232
- private_class_method :with_forced_hostname
233
219
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class SsrfFilter
4
- VERSION = '1.1.1'
4
+ VERSION = '1.2.0'
5
5
  end
data/lib/ssrf_filter.rb CHANGED
@@ -1,6 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'ssrf_filter/patch/resolv'
4
- require 'ssrf_filter/patch/ssl_socket'
5
3
  require 'ssrf_filter/ssrf_filter'
6
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.1
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: 2022-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,56 +58,56 @@ dependencies:
44
58
  requirements:
45
59
  - - "~>"
46
60
  - !ruby/object:Gem::Version
47
- version: 3.11.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.11.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
86
100
  requirements:
87
101
  - - "~>"
88
102
  - !ruby/object:Gem::Version
89
- version: 0.21.2
103
+ version: 0.22.0
90
104
  type: :development
91
105
  prerelease: false
92
106
  version_requirements: !ruby/object:Gem::Requirement
93
107
  requirements:
94
108
  - - "~>"
95
109
  - !ruby/object:Gem::Version
96
- version: 0.21.2
110
+ version: 0.22.0
97
111
  - !ruby/object:Gem::Dependency
98
112
  name: simplecov-lcov
99
113
  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,8 +158,6 @@ extensions: []
144
158
  extra_rdoc_files: []
145
159
  files:
146
160
  - lib/ssrf_filter.rb
147
- - lib/ssrf_filter/patch/resolv.rb
148
- - lib/ssrf_filter/patch/ssl_socket.rb
149
161
  - lib/ssrf_filter/ssrf_filter.rb
150
162
  - lib/ssrf_filter/version.rb
151
163
  homepage: https://github.com/arkadiyt/ssrf_filter
@@ -161,14 +173,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
161
173
  requirements:
162
174
  - - ">="
163
175
  - !ruby/object:Gem::Version
164
- version: 2.6.0
176
+ version: 2.7.0
165
177
  required_rubygems_version: !ruby/object:Gem::Requirement
166
178
  requirements:
167
179
  - - ">="
168
180
  - !ruby/object:Gem::Version
169
181
  version: '0'
170
182
  requirements: []
171
- rubygems_version: 3.0.3
183
+ rubygems_version: 3.3.7
172
184
  signing_key:
173
185
  specification_version: 4
174
186
  summary: A gem that makes it easy to prevent server side request forgery (SSRF) attacks
@@ -1,44 +0,0 @@
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,52 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class SsrfFilter
4
- module Patch
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
- def self.apply!
31
- return if instance_variable_defined?(:@patched_ssl_socket)
32
-
33
- @patched_ssl_socket = true
34
-
35
- ::OpenSSL::SSL::SSLSocket.class_eval do
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
- end
49
- end
50
- end
51
- end
52
- end