ssrf_filter 1.1.2 → 1.3.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 +4 -4
- data/lib/ssrf_filter/ssrf_filter.rb +22 -37
- data/lib/ssrf_filter/version.rb +1 -1
- data/lib/ssrf_filter.rb +0 -1
- metadata +20 -20
- data/lib/ssrf_filter/patch/ssl_socket.rb +0 -66
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 82909e9f3a32689d3a678e5474d09fbcfb8eae84308dd1ae13d622c8d1e6f6cd
|
4
|
+
data.tar.gz: 6c349dc635f346cb0204d1a238ab02e4f6adcc6e9d9cd4268404195dfa9a66b2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 602e11a98e4da6eaa0f869dc5f64aea715038b3a705b9354e25b7378dd96a893070bf4eb31d5c26c642b474d7891320c2749f7d9fc600e015c0e200cb04c3575
|
7
|
+
data.tar.gz: 4cd5d4382850e12b23784e807dc4728c8e67a7c214fa1aa8637fb10d49a570a1ca94ecacf9a2ef41d3c7ed983e2569545eacacdc80030dabbb99a6ef033e0e8c
|
@@ -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 (
|
13
|
+
while mask_addr.nobits?(0x1)
|
14
14
|
mask_addr >>= 1
|
15
15
|
end
|
16
16
|
|
@@ -84,8 +84,6 @@ class SsrfFilter
|
|
84
84
|
patch: ::Net::HTTP::Patch
|
85
85
|
}.freeze
|
86
86
|
|
87
|
-
FIBER_HOSTNAME_KEY = :__ssrf_filter_hostname
|
88
|
-
|
89
87
|
class Error < ::StandardError
|
90
88
|
end
|
91
89
|
|
@@ -106,8 +104,6 @@ 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
|
-
|
111
107
|
original_url = url
|
112
108
|
scheme_whitelist = options.fetch(:scheme_whitelist, DEFAULT_SCHEME_WHITELIST)
|
113
109
|
resolver = options.fetch(:resolver, DEFAULT_RESOLVER)
|
@@ -156,16 +152,16 @@ class SsrfFilter
|
|
156
152
|
end
|
157
153
|
private_class_method :ipaddr_has_mask?
|
158
154
|
|
159
|
-
def self.
|
155
|
+
def self.normalized_hostname(uri)
|
160
156
|
# Attach port for non-default as per RFC2616
|
161
157
|
if (uri.port == 80 && uri.scheme == 'http') ||
|
162
158
|
(uri.port == 443 && uri.scheme == 'https')
|
163
|
-
hostname
|
159
|
+
uri.hostname
|
164
160
|
else
|
165
|
-
"#{hostname}:#{uri.port}"
|
161
|
+
"#{uri.hostname}:#{uri.port}"
|
166
162
|
end
|
167
163
|
end
|
168
|
-
private_class_method :
|
164
|
+
private_class_method :normalized_hostname
|
169
165
|
|
170
166
|
def self.fetch_once(uri, ip, verb, options, &block)
|
171
167
|
if options[:params]
|
@@ -174,11 +170,8 @@ class SsrfFilter
|
|
174
170
|
uri.query = ::URI.encode_www_form(params)
|
175
171
|
end
|
176
172
|
|
177
|
-
hostname = uri.hostname
|
178
|
-
uri.hostname = ip
|
179
|
-
|
180
173
|
request = VERB_MAP[verb].new(uri)
|
181
|
-
request['host'] =
|
174
|
+
request['host'] = normalized_hostname(uri)
|
182
175
|
|
183
176
|
Array(options[:headers]).each do |header, value|
|
184
177
|
request[header] = value
|
@@ -189,24 +182,24 @@ class SsrfFilter
|
|
189
182
|
options[:request_proc].call(request) if options[:request_proc].respond_to?(:call)
|
190
183
|
validate_request(request)
|
191
184
|
|
192
|
-
http_options = options[:http_options] || {}
|
193
|
-
|
185
|
+
http_options = (options[:http_options] || {}).merge(
|
186
|
+
use_ssl: uri.scheme == 'https',
|
187
|
+
ipaddr: ip
|
188
|
+
)
|
194
189
|
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
url = nil
|
207
|
-
end
|
208
|
-
return response, url
|
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
|
209
201
|
end
|
202
|
+
return response, url
|
210
203
|
end
|
211
204
|
end
|
212
205
|
private_class_method :fetch_once
|
@@ -223,12 +216,4 @@ class SsrfFilter
|
|
223
216
|
end
|
224
217
|
end
|
225
218
|
private_class_method :validate_request
|
226
|
-
|
227
|
-
def self.with_forced_hostname(hostname, &_block)
|
228
|
-
::Thread.current[FIBER_HOSTNAME_KEY] = hostname
|
229
|
-
yield
|
230
|
-
ensure
|
231
|
-
::Thread.current[FIBER_HOSTNAME_KEY] = nil
|
232
|
-
end
|
233
|
-
private_class_method :with_forced_hostname
|
234
219
|
end
|
data/lib/ssrf_filter/version.rb
CHANGED
data/lib/ssrf_filter.rb
CHANGED
metadata
CHANGED
@@ -1,85 +1,85 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ssrf_filter
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.3.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:
|
11
|
+
date: 2025-05-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
|
-
name:
|
14
|
+
name: base64
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: 0.
|
19
|
+
version: 0.2.0
|
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.
|
26
|
+
version: 0.2.0
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
|
-
name:
|
28
|
+
name: bundler-audit
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
|
-
- - "
|
31
|
+
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version:
|
33
|
+
version: 0.9.2
|
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:
|
40
|
+
version: 0.9.2
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: rspec
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
45
|
- - "~>"
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version: 3.
|
47
|
+
version: 3.13.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: 3.
|
54
|
+
version: 3.13.0
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
56
|
name: rubocop
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
58
58
|
requirements:
|
59
59
|
- - "~>"
|
60
60
|
- !ruby/object:Gem::Version
|
61
|
-
version: 1.
|
61
|
+
version: 1.68.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: 1.
|
68
|
+
version: 1.68.0
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
70
|
name: rubocop-rspec
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
72
72
|
requirements:
|
73
73
|
- - "~>"
|
74
74
|
- !ruby/object:Gem::Version
|
75
|
-
version: 2.
|
75
|
+
version: 3.2.0
|
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: 2.
|
82
|
+
version: 3.2.0
|
83
83
|
- !ruby/object:Gem::Dependency
|
84
84
|
name: simplecov
|
85
85
|
requirement: !ruby/object:Gem::Requirement
|
@@ -114,14 +114,14 @@ dependencies:
|
|
114
114
|
requirements:
|
115
115
|
- - ">="
|
116
116
|
- !ruby/object:Gem::Version
|
117
|
-
version: 3.
|
117
|
+
version: 3.24.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.
|
124
|
+
version: 3.24.0
|
125
125
|
- !ruby/object:Gem::Dependency
|
126
126
|
name: webrick
|
127
127
|
requirement: !ruby/object:Gem::Requirement
|
@@ -144,13 +144,13 @@ extensions: []
|
|
144
144
|
extra_rdoc_files: []
|
145
145
|
files:
|
146
146
|
- lib/ssrf_filter.rb
|
147
|
-
- lib/ssrf_filter/patch/ssl_socket.rb
|
148
147
|
- lib/ssrf_filter/ssrf_filter.rb
|
149
148
|
- lib/ssrf_filter/version.rb
|
150
149
|
homepage: https://github.com/arkadiyt/ssrf_filter
|
151
150
|
licenses:
|
152
151
|
- MIT
|
153
152
|
metadata:
|
153
|
+
changelog_uri: https://github.com/arkadiyt/ssrf_filter/blob/main/CHANGELOG.md
|
154
154
|
rubygems_mfa_required: 'true'
|
155
155
|
post_install_message:
|
156
156
|
rdoc_options: []
|
@@ -160,7 +160,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
160
160
|
requirements:
|
161
161
|
- - ">="
|
162
162
|
- !ruby/object:Gem::Version
|
163
|
-
version: 2.
|
163
|
+
version: 2.7.0
|
164
164
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
165
165
|
requirements:
|
166
166
|
- - ">="
|
@@ -1,66 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
class SsrfFilter
|
4
|
-
module Patch
|
5
|
-
module SSLSocket
|
6
|
-
def self.apply!
|
7
|
-
return if instance_variable_defined?(:@patched_ssl_socket)
|
8
|
-
|
9
|
-
@patched_ssl_socket = true
|
10
|
-
|
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
|
-
|
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
|
-
|
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]
|
60
|
-
end
|
61
|
-
end
|
62
|
-
end
|
63
|
-
end
|
64
|
-
end
|
65
|
-
end
|
66
|
-
end
|