ssrf_filter 1.0.6 → 1.1.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 +5 -5
- data/lib/ssrf_filter/patch/ssl_socket.rb +3 -0
- data/lib/ssrf_filter/ssrf_filter.rb +30 -21
- data/lib/ssrf_filter/version.rb +1 -1
- data/lib/ssrf_filter.rb +0 -1
- metadata +80 -25
- data/lib/ssrf_filter/patch/http_generic_request.rb +0 -83
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 97b1c63593031040d2f23b6fbb055c44490fc2624a5b9901203053c971991ba6
|
4
|
+
data.tar.gz: ca816401abe0177feea5fd70d01afd19758598cd9864659a8eb97e7184367830
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 75ae3f265b486cb8768ef0c49d3dd0313a218b3863a2c1dfc8615727453aea45ab7ae5f71289e3748eecb9e17dc09443870413937c7fbad56a9a7a225b2fbb89
|
7
|
+
data.tar.gz: c881ea003dafd105bbbff4001646a879fc4cefb18d722fc60f21281c8ebaca8b19a324dccadb9acdf799e56501905addebcfe8f91253ee52136a9dab433335ad
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
class SsrfFilter
|
2
4
|
module Patch
|
3
5
|
module SSLSocket
|
@@ -27,6 +29,7 @@ class SsrfFilter
|
|
27
29
|
|
28
30
|
def self.apply!
|
29
31
|
return if instance_variable_defined?(:@patched_ssl_socket)
|
32
|
+
|
30
33
|
@patched_ssl_socket = true
|
31
34
|
|
32
35
|
::OpenSSL::SSL::SSLSocket.class_eval do
|
@@ -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
|
@@ -53,11 +55,12 @@ class SsrfFilter
|
|
53
55
|
::IPAddr.new('2002::/16'), # 6to4
|
54
56
|
::IPAddr.new('fc00::/7'), # Unique local address
|
55
57
|
::IPAddr.new('fe80::/10'), # Link-local address
|
56
|
-
::IPAddr.new('ff00::/8')
|
58
|
+
::IPAddr.new('ff00::/8') # Multicast
|
57
59
|
] + IPV4_BLACKLIST.flat_map do |ipaddr|
|
58
60
|
prefixlen = prefixlen_from_ipaddr(ipaddr)
|
59
61
|
|
60
|
-
|
62
|
+
# Don't call ipaddr.ipv4_compat because it prints out a deprecation warning on ruby 2.5+
|
63
|
+
ipv4_compatible = IPAddr.new(ipaddr.to_i, Socket::AF_INET6).mask(96 + prefixlen)
|
61
64
|
ipv4_mapped = ipaddr.ipv4_mapped.mask(80 + prefixlen)
|
62
65
|
|
63
66
|
[ipv4_compatible, ipv4_mapped]
|
@@ -75,7 +78,9 @@ class SsrfFilter
|
|
75
78
|
get: ::Net::HTTP::Get,
|
76
79
|
put: ::Net::HTTP::Put,
|
77
80
|
post: ::Net::HTTP::Post,
|
78
|
-
delete: ::Net::HTTP::Delete
|
81
|
+
delete: ::Net::HTTP::Delete,
|
82
|
+
head: ::Net::HTTP::Head,
|
83
|
+
patch: ::Net::HTTP::Patch
|
79
84
|
}.freeze
|
80
85
|
|
81
86
|
FIBER_LOCAL_KEY = :__ssrf_filter_hostname
|
@@ -98,10 +103,9 @@ class SsrfFilter
|
|
98
103
|
class CRLFInjection < Error
|
99
104
|
end
|
100
105
|
|
101
|
-
%i[get put post delete].each do |method|
|
106
|
+
%i[get put post delete head patch].each do |method|
|
102
107
|
define_singleton_method(method) do |url, options = {}, &block|
|
103
108
|
::SsrfFilter::Patch::SSLSocket.apply!
|
104
|
-
::SsrfFilter::Patch::HTTPGenericRequest.apply!
|
105
109
|
|
106
110
|
original_url = url
|
107
111
|
scheme_whitelist = options[:scheme_whitelist] || DEFAULT_SCHEME_WHITELIST
|
@@ -123,16 +127,8 @@ class SsrfFilter
|
|
123
127
|
public_addresses = ip_addresses.reject(&method(:unsafe_ip_address?))
|
124
128
|
raise PrivateIPAddress, "Hostname '#{hostname}' has no public ip addresses" if public_addresses.empty?
|
125
129
|
|
126
|
-
response = fetch_once(uri, public_addresses.sample.to_s, method, options, &block)
|
127
|
-
|
128
|
-
case response
|
129
|
-
when ::Net::HTTPRedirection then
|
130
|
-
url = response['location']
|
131
|
-
# Handle relative redirects
|
132
|
-
url = "#{uri.scheme}://#{hostname}:#{uri.port}#{url}" if url.start_with?('/')
|
133
|
-
else
|
134
|
-
return response
|
135
|
-
end
|
130
|
+
response, url = fetch_once(uri, public_addresses.sample.to_s, method, options, &block)
|
131
|
+
return response if url.nil?
|
136
132
|
end
|
137
133
|
|
138
134
|
raise TooManyRedirects, "Got #{max_redirects} redirects fetching #{original_url}"
|
@@ -168,7 +164,7 @@ class SsrfFilter
|
|
168
164
|
|
169
165
|
def self.fetch_once(uri, ip, verb, options, &block)
|
170
166
|
if options[:params]
|
171
|
-
params = uri.query ? ::
|
167
|
+
params = uri.query ? ::URI.decode_www_form(uri.query).to_h : {}
|
172
168
|
params.merge!(options[:params])
|
173
169
|
uri.query = ::URI.encode_www_form(params)
|
174
170
|
end
|
@@ -185,13 +181,26 @@ class SsrfFilter
|
|
185
181
|
|
186
182
|
request.body = options[:body] if options[:body]
|
187
183
|
|
188
|
-
|
184
|
+
options[:request_proc].call(request) if options[:request_proc].respond_to?(:call)
|
189
185
|
validate_request(request)
|
190
186
|
|
191
|
-
|
187
|
+
http_options = options[:http_options] || {}
|
188
|
+
http_options[:use_ssl] = (uri.scheme == 'https')
|
189
|
+
|
192
190
|
with_forced_hostname(hostname) do
|
193
|
-
::Net::HTTP.start(uri.hostname, uri.port,
|
194
|
-
http.request(request)
|
191
|
+
::Net::HTTP.start(uri.hostname, uri.port, **http_options) do |http|
|
192
|
+
http.request(request) do |response|
|
193
|
+
case response
|
194
|
+
when ::Net::HTTPRedirection
|
195
|
+
url = response['location']
|
196
|
+
# Handle relative redirects
|
197
|
+
url = "#{uri.scheme}://#{hostname}:#{uri.port}#{url}" if url.start_with?('/')
|
198
|
+
return nil, url
|
199
|
+
else
|
200
|
+
block&.call(response)
|
201
|
+
return response, nil
|
202
|
+
end
|
203
|
+
end
|
195
204
|
end
|
196
205
|
end
|
197
206
|
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.0
|
4
|
+
version: 1.1.0
|
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-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler-audit
|
@@ -16,87 +16,143 @@ 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/http_generic_request.rb
|
92
147
|
- lib/ssrf_filter/patch/ssl_socket.rb
|
93
148
|
- lib/ssrf_filter/ssrf_filter.rb
|
94
149
|
- lib/ssrf_filter/version.rb
|
95
150
|
homepage: https://github.com/arkadiyt/ssrf_filter
|
96
151
|
licenses:
|
97
152
|
- MIT
|
98
|
-
metadata:
|
99
|
-
|
153
|
+
metadata:
|
154
|
+
rubygems_mfa_required: 'true'
|
155
|
+
post_install_message:
|
100
156
|
rdoc_options: []
|
101
157
|
require_paths:
|
102
158
|
- lib
|
@@ -104,16 +160,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
104
160
|
requirements:
|
105
161
|
- - ">="
|
106
162
|
- !ruby/object:Gem::Version
|
107
|
-
version: 2.
|
163
|
+
version: 2.6.0
|
108
164
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
109
165
|
requirements:
|
110
166
|
- - ">="
|
111
167
|
- !ruby/object:Gem::Version
|
112
168
|
version: '0'
|
113
169
|
requirements: []
|
114
|
-
|
115
|
-
|
116
|
-
signing_key:
|
170
|
+
rubygems_version: 3.0.3
|
171
|
+
signing_key:
|
117
172
|
specification_version: 4
|
118
173
|
summary: A gem that makes it easy to prevent server side request forgery (SSRF) attacks
|
119
174
|
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
|