ssrf_filter 1.1.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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