ssrf_filter 1.0.7 → 1.1.1
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/patch/resolv.rb +44 -0
- data/lib/ssrf_filter/patch/ssl_socket.rb +5 -2
- data/lib/ssrf_filter/ssrf_filter.rb +33 -23
- data/lib/ssrf_filter/version.rb +1 -1
- data/lib/ssrf_filter.rb +1 -1
- metadata +80 -23
- data/lib/ssrf_filter/patch/http_generic_request.rb +0 -83
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f0230e20c7cd24b24dad277ee01042f3a4885fa68a8ea593fe0607de92023026
|
4
|
+
data.tar.gz: 6ce71a83907b1acfc382acc5d859a26fc1471dc3cd2dbc48d14b3e1dfca12051
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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::
|
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::
|
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
|
-
|
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
|
-
|
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::
|
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 ? ::
|
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
|
-
|
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[
|
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[
|
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
|
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.
|
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:
|
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.
|
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.
|
26
|
+
version: 0.9.1
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
|
-
name:
|
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:
|
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:
|
54
|
+
version: 3.11.0
|
41
55
|
- !ruby/object:Gem::Dependency
|
42
|
-
name:
|
56
|
+
name: rubocop
|
43
57
|
requirement: !ruby/object:Gem::Requirement
|
44
58
|
requirements:
|
45
59
|
- - "~>"
|
46
60
|
- !ruby/object:Gem::Version
|
47
|
-
version:
|
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:
|
68
|
+
version: 1.35.0
|
55
69
|
- !ruby/object:Gem::Dependency
|
56
|
-
name:
|
70
|
+
name: rubocop-rspec
|
57
71
|
requirement: !ruby/object:Gem::Requirement
|
58
72
|
requirements:
|
59
73
|
- - "~>"
|
60
74
|
- !ruby/object:Gem::Version
|
61
|
-
version:
|
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:
|
82
|
+
version: 2.12.1
|
69
83
|
- !ruby/object:Gem::Dependency
|
70
|
-
name:
|
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.
|
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.
|
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/
|
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
|
-
|
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.
|
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
|