ssrf_filter 1.0.5 → 1.0.8
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 +5 -5
- data/lib/ssrf_filter/patch/http_generic_request.rb +83 -0
- data/lib/ssrf_filter/patch/ssl_socket.rb +49 -0
- data/lib/ssrf_filter/ssrf_filter.rb +14 -51
- data/lib/ssrf_filter/version.rb +1 -1
- data/lib/ssrf_filter.rb +2 -0
- metadata +77 -20
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 3ad08c7683abaf9c1cce31358e91eebd54e2276911da99c3d7bdbc1481ec6a8f
|
4
|
+
data.tar.gz: 4993290726f69399264c18dfe8b4ef1c908b78c726f0cfb5dcbc5b9c09043ead
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 522368253a813feb3ac735f419b8d9fe6ac3b006debf3e3c003f6909d809b5b51af06ccc9c42b7cd8d18e90d54a298d3a39fee0514304a1c3a2a1ffd1d54580d
|
7
|
+
data.tar.gz: 21cd1fe841b77536db7e51e9cddc94b7ed96305bf766bb16df77e5b83029a6b6c44e042d1036ae52a135419e72ae70ec1155c471fdc6596eb9dab07809e54e36
|
@@ -0,0 +1,83 @@
|
|
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
|
@@ -0,0 +1,49 @@
|
|
1
|
+
class SsrfFilter
|
2
|
+
module Patch
|
3
|
+
module SSLSocket
|
4
|
+
# When fetching a url we'd like to have the following workflow:
|
5
|
+
# 1) resolve the hostname www.example.com, and choose a public ip address to connect to
|
6
|
+
# 2) connect to that specific ip address, to prevent things like DNS TOCTTOU bugs or other trickery
|
7
|
+
#
|
8
|
+
# Ideally this would happen by the ruby http library giving us control over DNS resolution,
|
9
|
+
# but it doesn't. Instead, when making the request we set the uri.hostname to the chosen ip address,
|
10
|
+
# and send a 'Host' header of the original hostname, i.e. connect to 'http://93.184.216.34/' and send
|
11
|
+
# a 'Host: www.example.com' header.
|
12
|
+
#
|
13
|
+
# This works for the http case, http://www.example.com. For the https case, this causes certificate
|
14
|
+
# validation failures, since the server certificate for https://www.example.com will not have a
|
15
|
+
# Subject Alternate Name for 93.184.216.34.
|
16
|
+
#
|
17
|
+
# Thus we perform the monkey-patch below, modifying SSLSocket's `post_connection_check(hostname)`
|
18
|
+
# and `hostname=(hostname)` methods:
|
19
|
+
# If our fiber local variable is set, use that for the hostname instead, otherwise behave as usual.
|
20
|
+
# The only time the variable will be set is if you are executing inside a `with_forced_hostname` block,
|
21
|
+
# which is used in ssrf_filter.rb.
|
22
|
+
#
|
23
|
+
# An alternative approach could be to pass in our own OpenSSL::X509::Store with a custom
|
24
|
+
# `verify_callback` to the ::Net::HTTP.start call, but this would require reimplementing certification
|
25
|
+
# validation, which is dangerous. This way we can piggyback on the existing validation and simply pretend
|
26
|
+
# that we connected to the desired hostname.
|
27
|
+
|
28
|
+
def self.apply!
|
29
|
+
return if instance_variable_defined?(:@patched_ssl_socket)
|
30
|
+
|
31
|
+
@patched_ssl_socket = true
|
32
|
+
|
33
|
+
::OpenSSL::SSL::SSLSocket.class_eval do
|
34
|
+
original_post_connection_check = instance_method(:post_connection_check)
|
35
|
+
define_method(:post_connection_check) do |hostname|
|
36
|
+
original_post_connection_check.bind(self).call(::Thread.current[::SsrfFilter::FIBER_LOCAL_KEY] || hostname)
|
37
|
+
end
|
38
|
+
|
39
|
+
if method_defined?(:hostname=)
|
40
|
+
original_hostname = instance_method(:hostname=)
|
41
|
+
define_method(:hostname=) do |hostname|
|
42
|
+
original_hostname.bind(self).call(::Thread.current[::SsrfFilter::FIBER_LOCAL_KEY] || hostname)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -53,11 +53,12 @@ class SsrfFilter
|
|
53
53
|
::IPAddr.new('2002::/16'), # 6to4
|
54
54
|
::IPAddr.new('fc00::/7'), # Unique local address
|
55
55
|
::IPAddr.new('fe80::/10'), # Link-local address
|
56
|
-
::IPAddr.new('ff00::/8')
|
56
|
+
::IPAddr.new('ff00::/8') # Multicast
|
57
57
|
] + IPV4_BLACKLIST.flat_map do |ipaddr|
|
58
58
|
prefixlen = prefixlen_from_ipaddr(ipaddr)
|
59
59
|
|
60
|
-
|
60
|
+
# Don't call ipaddr.ipv4_compat because it prints out a deprecation warning on ruby 2.5+
|
61
|
+
ipv4_compatible = IPAddr.new(ipaddr.to_i, Socket::AF_INET6).mask(96 + prefixlen)
|
61
62
|
ipv4_mapped = ipaddr.ipv4_mapped.mask(80 + prefixlen)
|
62
63
|
|
63
64
|
[ipv4_compatible, ipv4_mapped]
|
@@ -75,7 +76,9 @@ class SsrfFilter
|
|
75
76
|
get: ::Net::HTTP::Get,
|
76
77
|
put: ::Net::HTTP::Put,
|
77
78
|
post: ::Net::HTTP::Post,
|
78
|
-
delete: ::Net::HTTP::Delete
|
79
|
+
delete: ::Net::HTTP::Delete,
|
80
|
+
head: ::Net::HTTP::Head,
|
81
|
+
patch: ::Net::HTTP::Patch
|
79
82
|
}.freeze
|
80
83
|
|
81
84
|
FIBER_LOCAL_KEY = :__ssrf_filter_hostname
|
@@ -98,8 +101,11 @@ class SsrfFilter
|
|
98
101
|
class CRLFInjection < Error
|
99
102
|
end
|
100
103
|
|
101
|
-
%i[get put post delete].each do |method|
|
104
|
+
%i[get put post delete head patch].each do |method|
|
102
105
|
define_singleton_method(method) do |url, options = {}, &block|
|
106
|
+
::SsrfFilter::Patch::SSLSocket.apply!
|
107
|
+
::SsrfFilter::Patch::HTTPGenericRequest.apply!
|
108
|
+
|
103
109
|
original_url = url
|
104
110
|
scheme_whitelist = options[:scheme_whitelist] || DEFAULT_SCHEME_WHITELIST
|
105
111
|
resolver = options[:resolver] || DEFAULT_RESOLVER
|
@@ -185,9 +191,11 @@ class SsrfFilter
|
|
185
191
|
block.call(request) if block_given?
|
186
192
|
validate_request(request)
|
187
193
|
|
188
|
-
|
194
|
+
http_options = options[:http_options] || {}
|
195
|
+
http_options[:use_ssl] = (uri.scheme == 'https')
|
196
|
+
|
189
197
|
with_forced_hostname(hostname) do
|
190
|
-
::Net::HTTP.start(uri.hostname, uri.port,
|
198
|
+
::Net::HTTP.start(uri.hostname, uri.port, http_options) do |http|
|
191
199
|
http.request(request)
|
192
200
|
end
|
193
201
|
end
|
@@ -207,52 +215,7 @@ class SsrfFilter
|
|
207
215
|
end
|
208
216
|
private_class_method :validate_request
|
209
217
|
|
210
|
-
def self.patch_ssl_socket!
|
211
|
-
return if instance_variable_defined?(:@patched_ssl_socket)
|
212
|
-
|
213
|
-
# What we'd like to do is have the following workflow:
|
214
|
-
# 1) resolve the hostname www.example.com, and choose a public ip address to connect to
|
215
|
-
# 2) connect to that specific ip address, to prevent things like DNS TOCTTOU bugs or other trickery
|
216
|
-
#
|
217
|
-
# Ideally this would happen by the ruby http library giving us control over DNS resolution,
|
218
|
-
# but it doesn't. Instead, when making the request we set the uri.hostname to the chosen ip address,
|
219
|
-
# and send a 'Host' header of the original hostname, i.e. connect to 'http://93.184.216.34/' and send
|
220
|
-
# a 'Host: www.example.com' header.
|
221
|
-
#
|
222
|
-
# This works for the http case, http://www.example.com. For the https case, this causes certificate
|
223
|
-
# validation failures, since the server certificate does not have a Subject Alternate Name for 93.184.216.34.
|
224
|
-
#
|
225
|
-
# Thus we perform the monkey-patch below, modifying SSLSocket's `post_connection_check(hostname)`
|
226
|
-
# and `hostname=(hostname)` methods:
|
227
|
-
# If our fiber local variable is set, use that for the hostname instead, otherwise behave as usual.
|
228
|
-
# The only time the variable will be set is if you are executing inside a `with_forced_hostname` block,
|
229
|
-
# which is used above.
|
230
|
-
#
|
231
|
-
# An alternative approach could be to pass in our own OpenSSL::X509::Store with a custom
|
232
|
-
# `verify_callback` to the ::Net::HTTP.start call, but this would require reimplementing certification
|
233
|
-
# validation, which is dangerous. This way we can piggyback on the existing validation and simply pretend
|
234
|
-
# that we connected to the desired hostname.
|
235
|
-
|
236
|
-
::OpenSSL::SSL::SSLSocket.class_eval do
|
237
|
-
original_post_connection_check = instance_method(:post_connection_check)
|
238
|
-
define_method(:post_connection_check) do |hostname|
|
239
|
-
original_post_connection_check.bind(self).call(::Thread.current[FIBER_LOCAL_KEY] || hostname)
|
240
|
-
end
|
241
|
-
|
242
|
-
if method_defined?(:hostname=)
|
243
|
-
original_hostname = instance_method(:hostname=)
|
244
|
-
define_method(:hostname=) do |hostname|
|
245
|
-
original_hostname.bind(self).call(::Thread.current[FIBER_LOCAL_KEY] || hostname)
|
246
|
-
end
|
247
|
-
end
|
248
|
-
end
|
249
|
-
|
250
|
-
@patched_ssl_socket = true
|
251
|
-
end
|
252
|
-
private_class_method :patch_ssl_socket!
|
253
|
-
|
254
218
|
def self.with_forced_hostname(hostname, &_block)
|
255
|
-
patch_ssl_socket!
|
256
219
|
::Thread.current[FIBER_LOCAL_KEY] = hostname
|
257
220
|
yield
|
258
221
|
ensure
|
data/lib/ssrf_filter/version.rb
CHANGED
data/lib/ssrf_filter.rb
CHANGED
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.
|
4
|
+
version: 1.0.8
|
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:
|
11
|
+
date: 2022-08-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler-audit
|
@@ -16,85 +16,143 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: 0.6.
|
19
|
+
version: 0.6.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.
|
26
|
+
version: 0.6.1
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: coveralls
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
31
|
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: 0.8.
|
33
|
+
version: 0.8.22
|
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.
|
40
|
+
version: 0.8.22
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: psych
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "<"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '4'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "<"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '4'
|
41
55
|
- !ruby/object:Gem::Dependency
|
42
56
|
name: rspec
|
43
57
|
requirement: !ruby/object:Gem::Requirement
|
44
58
|
requirements:
|
45
59
|
- - "~>"
|
46
60
|
- !ruby/object:Gem::Version
|
47
|
-
version: 3.
|
61
|
+
version: 3.8.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.
|
68
|
+
version: 3.8.0
|
55
69
|
- !ruby/object:Gem::Dependency
|
56
70
|
name: webmock
|
57
71
|
requirement: !ruby/object:Gem::Requirement
|
58
72
|
requirements:
|
59
|
-
- - "
|
73
|
+
- - ">="
|
60
74
|
- !ruby/object:Gem::Version
|
61
|
-
version: 3.
|
75
|
+
version: 3.5.1
|
62
76
|
type: :development
|
63
77
|
prerelease: false
|
64
78
|
version_requirements: !ruby/object:Gem::Requirement
|
65
79
|
requirements:
|
66
|
-
- - "
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 3.5.1
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: webrick
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: public_suffix
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - '='
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: 2.0.5
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - '='
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: 2.0.5
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: rexml
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - '='
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: 3.2.4
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - '='
|
67
123
|
- !ruby/object:Gem::Version
|
68
|
-
version: 3.2.
|
124
|
+
version: 3.2.4
|
69
125
|
- !ruby/object:Gem::Dependency
|
70
126
|
name: rubocop
|
71
127
|
requirement: !ruby/object:Gem::Requirement
|
72
128
|
requirements:
|
73
129
|
- - "~>"
|
74
130
|
- !ruby/object:Gem::Version
|
75
|
-
version: 0.
|
131
|
+
version: 0.50.0
|
76
132
|
type: :development
|
77
133
|
prerelease: false
|
78
134
|
version_requirements: !ruby/object:Gem::Requirement
|
79
135
|
requirements:
|
80
136
|
- - "~>"
|
81
137
|
- !ruby/object:Gem::Version
|
82
|
-
version: 0.
|
138
|
+
version: 0.50.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
|
147
|
+
- lib/ssrf_filter/patch/http_generic_request.rb
|
148
|
+
- lib/ssrf_filter/patch/ssl_socket.rb
|
91
149
|
- lib/ssrf_filter/ssrf_filter.rb
|
92
150
|
- lib/ssrf_filter/version.rb
|
93
151
|
homepage: https://github.com/arkadiyt/ssrf_filter
|
94
152
|
licenses:
|
95
153
|
- MIT
|
96
154
|
metadata: {}
|
97
|
-
post_install_message:
|
155
|
+
post_install_message:
|
98
156
|
rdoc_options: []
|
99
157
|
require_paths:
|
100
158
|
- lib
|
@@ -109,9 +167,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
109
167
|
- !ruby/object:Gem::Version
|
110
168
|
version: '0'
|
111
169
|
requirements: []
|
112
|
-
|
113
|
-
|
114
|
-
signing_key:
|
170
|
+
rubygems_version: 3.2.15
|
171
|
+
signing_key:
|
115
172
|
specification_version: 4
|
116
173
|
summary: A gem that makes it easy to prevent server side request forgery (SSRF) attacks
|
117
174
|
test_files: []
|