ssrf_filter 1.4.0 → 1.6.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 +65 -3
- data/lib/ssrf_filter/version.rb +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '05509e34ae84e22f9e746604ec7717a095ea88a237c13dfabb2d840e24b38816'
|
|
4
|
+
data.tar.gz: 1c6694c52b643ec4992fd19c1822eb225c4e87d9cf75bd2b1b2654a5f06f9b10
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 352ca8aa91c7408f1b84d0791378e463da9e356feab9559209a05c43049879acf5f88445126fbb9532ce979a5ffed7960a057d9409399f5cba805cb062e19761
|
|
7
|
+
data.tar.gz: dbbdbb309c241adc37d81dc2f7a128834ce0bf04f0b87662121280aa37e81ddce6a39b2a206972fc63d72c492db81108ec67165bb7d3d5b8be45f1c46c579a55
|
|
@@ -59,13 +59,18 @@ class SsrfFilter
|
|
|
59
59
|
NAT64_LOCAL_PREFIX = ::IPAddr.new('64:ff9b:1::/48').freeze
|
|
60
60
|
|
|
61
61
|
IPV6_BLACKLIST = ([
|
|
62
|
+
::IPAddr.new('::/128'), # Unspecified address
|
|
62
63
|
::IPAddr.new('::1/128'), # Loopback
|
|
63
64
|
::IPAddr.new('100::/64'), # Discard prefix (RFC 6666)
|
|
65
|
+
::IPAddr.new('100:0:0:1::/64'), # Dummy IPv6 prefix (RFC 9780)
|
|
64
66
|
::IPAddr.new('2001::/32'), # Teredo tunneling
|
|
67
|
+
::IPAddr.new('2001:2::/48'), # Benchmarking (RFC 5180)
|
|
65
68
|
::IPAddr.new('2001:10::/28'), # Deprecated (previously ORCHID)
|
|
66
69
|
::IPAddr.new('2001:20::/28'), # ORCHIDv2
|
|
67
70
|
::IPAddr.new('2001:db8::/32'), # Addresses used in documentation and example source code
|
|
68
71
|
::IPAddr.new('2002::/16'), # 6to4
|
|
72
|
+
::IPAddr.new('3fff::/20'), # Documentation (RFC 9637)
|
|
73
|
+
::IPAddr.new('5f00::/16'), # Segment Routing (SRv6) SIDs (RFC 9602)
|
|
69
74
|
::IPAddr.new('fc00::/7'), # Unique local address
|
|
70
75
|
::IPAddr.new('fe80::/10'), # Link-local address
|
|
71
76
|
::IPAddr.new('ff00::/8') # Multicast
|
|
@@ -91,6 +96,17 @@ class SsrfFilter
|
|
|
91
96
|
|
|
92
97
|
DEFAULT_ALLOW_UNFOLLOWED_REDIRECTS = false
|
|
93
98
|
DEFAULT_MAX_REDIRECTS = 10
|
|
99
|
+
DEFAULT_SENSITIVE_HEADERS = %w[authorization cookie].freeze
|
|
100
|
+
DEFAULT_ON_CROSS_ORIGIN_REDIRECT = :strip
|
|
101
|
+
|
|
102
|
+
CONNECTION_ERRORS = [
|
|
103
|
+
::Errno::EHOSTUNREACH,
|
|
104
|
+
::Errno::ENETUNREACH,
|
|
105
|
+
::Errno::EADDRNOTAVAIL,
|
|
106
|
+
::Errno::ECONNREFUSED,
|
|
107
|
+
::Errno::ETIMEDOUT,
|
|
108
|
+
::Net::OpenTimeout
|
|
109
|
+
].freeze
|
|
94
110
|
|
|
95
111
|
VERB_MAP = {
|
|
96
112
|
get: ::Net::HTTP::Get,
|
|
@@ -119,14 +135,19 @@ class SsrfFilter
|
|
|
119
135
|
class CRLFInjection < Error
|
|
120
136
|
end
|
|
121
137
|
|
|
138
|
+
class CredentialLeakage < Error
|
|
139
|
+
end
|
|
140
|
+
|
|
122
141
|
VERB_MAP.each_key do |method|
|
|
123
142
|
define_singleton_method(method) do |url, options = {}, &block|
|
|
143
|
+
url = url.to_s
|
|
124
144
|
original_url = url
|
|
145
|
+
original_uri = URI(url)
|
|
125
146
|
scheme_whitelist = options.fetch(:scheme_whitelist, DEFAULT_SCHEME_WHITELIST)
|
|
126
147
|
resolver = options.fetch(:resolver, DEFAULT_RESOLVER)
|
|
127
148
|
allow_unfollowed_redirects = options.fetch(:allow_unfollowed_redirects, DEFAULT_ALLOW_UNFOLLOWED_REDIRECTS)
|
|
128
149
|
max_redirects = options.fetch(:max_redirects, DEFAULT_MAX_REDIRECTS)
|
|
129
|
-
|
|
150
|
+
sensitive_headers = options.fetch(:sensitive_headers, DEFAULT_SENSITIVE_HEADERS)
|
|
130
151
|
|
|
131
152
|
response = nil
|
|
132
153
|
(max_redirects + 1).times do
|
|
@@ -143,7 +164,14 @@ class SsrfFilter
|
|
|
143
164
|
public_addresses = ip_addresses.reject(&method(:unsafe_ip_address?))
|
|
144
165
|
raise PrivateIPAddress, "Hostname '#{hostname}' has no public ip addresses" if public_addresses.empty?
|
|
145
166
|
|
|
146
|
-
|
|
167
|
+
headers_to_strip = if !sensitive_headers.empty? && different_origin?(original_uri, uri)
|
|
168
|
+
sensitive_headers
|
|
169
|
+
else
|
|
170
|
+
[]
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
response, url = fetch_with_fallback(uri, public_addresses, method,
|
|
174
|
+
options.merge(headers_to_strip: headers_to_strip), &block)
|
|
147
175
|
return response if url.nil?
|
|
148
176
|
end
|
|
149
177
|
|
|
@@ -178,6 +206,10 @@ class SsrfFilter
|
|
|
178
206
|
range.first != range.last
|
|
179
207
|
end
|
|
180
208
|
|
|
209
|
+
private_class_method def self.different_origin?(uri1, uri2)
|
|
210
|
+
uri1.scheme != uri2.scheme || uri1.hostname != uri2.hostname || uri1.port != uri2.port
|
|
211
|
+
end
|
|
212
|
+
|
|
181
213
|
private_class_method def self.normalized_hostname(uri)
|
|
182
214
|
# Attach port for non-default as per RFC2616
|
|
183
215
|
if (uri.port == 80 && uri.scheme == 'http') ||
|
|
@@ -188,6 +220,18 @@ class SsrfFilter
|
|
|
188
220
|
end
|
|
189
221
|
end
|
|
190
222
|
|
|
223
|
+
private_class_method def self.fetch_with_fallback(uri, public_addresses, verb, options, &block)
|
|
224
|
+
last_error = nil
|
|
225
|
+
|
|
226
|
+
public_addresses.shuffle.each do |ip_address|
|
|
227
|
+
return fetch_once(uri.dup, ip_address.to_s, verb, options, &block)
|
|
228
|
+
rescue *CONNECTION_ERRORS => e
|
|
229
|
+
last_error = e
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
raise last_error
|
|
233
|
+
end
|
|
234
|
+
|
|
191
235
|
private_class_method def self.fetch_once(uri, ip, verb, options, &block)
|
|
192
236
|
if options[:params]
|
|
193
237
|
params = uri.query ? ::URI.decode_www_form(uri.query).to_h : {}
|
|
@@ -199,12 +243,30 @@ class SsrfFilter
|
|
|
199
243
|
request['host'] = normalized_hostname(uri)
|
|
200
244
|
|
|
201
245
|
Array(options[:headers]).each do |header, value|
|
|
246
|
+
# Newer rubies raise ArgumentError when assigning a header name with control characters, which
|
|
247
|
+
# would pre-empt the CRLFInjection check in validate_request, so reject it with our own error here
|
|
248
|
+
raise CRLFInjection, "CRLF injection in header #{header} with value #{value}" if header.to_s.count("\r\n") != 0
|
|
249
|
+
|
|
202
250
|
request[header] = value
|
|
203
251
|
end
|
|
204
252
|
|
|
205
253
|
request.body = options[:body] if options[:body]
|
|
206
254
|
|
|
207
255
|
options[:request_proc].call(request) if options[:request_proc].respond_to?(:call)
|
|
256
|
+
|
|
257
|
+
headers_to_strip = Array(options[:headers_to_strip])
|
|
258
|
+
unless headers_to_strip.empty?
|
|
259
|
+
if options[:on_cross_origin_redirect] == :raise
|
|
260
|
+
leaking = headers_to_strip.select { |h| request[h] }
|
|
261
|
+
unless leaking.empty?
|
|
262
|
+
raise CredentialLeakage,
|
|
263
|
+
"Cross-origin redirect would leak sensitive headers: #{leaking.join(', ')}"
|
|
264
|
+
end
|
|
265
|
+
else
|
|
266
|
+
headers_to_strip.each { |h| request.delete(h) }
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
|
|
208
270
|
validate_request(request)
|
|
209
271
|
|
|
210
272
|
http_options = (options[:http_options] || {}).merge(
|
|
@@ -212,7 +274,7 @@ class SsrfFilter
|
|
|
212
274
|
ipaddr: ip
|
|
213
275
|
)
|
|
214
276
|
|
|
215
|
-
::Net::HTTP.start(uri.hostname, uri.port, **http_options) do |http|
|
|
277
|
+
::Net::HTTP.start(uri.hostname, uri.port, nil, **http_options) do |http|
|
|
216
278
|
response = http.request(request) do |res|
|
|
217
279
|
block&.call(res)
|
|
218
280
|
end
|
data/lib/ssrf_filter/version.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.6.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: 2026-
|
|
11
|
+
date: 2026-09-09 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: base64
|
|
@@ -181,7 +181,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
181
181
|
- !ruby/object:Gem::Version
|
|
182
182
|
version: '0'
|
|
183
183
|
requirements: []
|
|
184
|
-
rubygems_version: 3.
|
|
184
|
+
rubygems_version: 3.3.7
|
|
185
185
|
signing_key:
|
|
186
186
|
specification_version: 4
|
|
187
187
|
summary: A gem that makes it easy to prevent server side request forgery (SSRF) attacks
|