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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8044ad57435e5aa1af62c8defb3087ee7dce64fdfb9530b8d5d73a0793607f6e
4
- data.tar.gz: 914546724fc94a99915d4c710c21e6ec89f24f5aa3b32cca8a2ec4613719a616
3
+ metadata.gz: '05509e34ae84e22f9e746604ec7717a095ea88a237c13dfabb2d840e24b38816'
4
+ data.tar.gz: 1c6694c52b643ec4992fd19c1822eb225c4e87d9cf75bd2b1b2654a5f06f9b10
5
5
  SHA512:
6
- metadata.gz: 9b5842cc8b3a2b4bef7f82cd3956def19d1d96e7cb85c6524a36bffe4f04cfaf58a01630203d0ab824063507802c4a681503199accc3154e684b0f2bbd594b47
7
- data.tar.gz: 494fff3091b5deb2aa76507a482114a55375322395f9e9fb4cf4139b1fabc5415d8ffa5a31497eb05217b70d8793960c4a808967536c64605f9787b776c07d92
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
- url = url.to_s
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
- response, url = fetch_once(uri, public_addresses.sample.to_s, method, options, &block)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class SsrfFilter
4
- VERSION = '1.4.0'
4
+ VERSION = '1.6.0'
5
5
  end
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.0
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-03-31 00:00:00.000000000 Z
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.5.16
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