ssrf_filter 1.3.0 → 1.4.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 +43 -20
- data/lib/ssrf_filter/version.rb +1 -1
- metadata +17 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8044ad57435e5aa1af62c8defb3087ee7dce64fdfb9530b8d5d73a0793607f6e
|
|
4
|
+
data.tar.gz: 914546724fc94a99915d4c710c21e6ec89f24f5aa3b32cca8a2ec4613719a616
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9b5842cc8b3a2b4bef7f82cd3956def19d1d96e7cb85c6524a36bffe4f04cfaf58a01630203d0ab824063507802c4a681503199accc3154e684b0f2bbd594b47
|
|
7
|
+
data.tar.gz: 494fff3091b5deb2aa76507a482114a55375322395f9e9fb4cf4139b1fabc5415d8ffa5a31497eb05217b70d8793960c4a808967536c64605f9787b776c07d92
|
|
@@ -6,7 +6,7 @@ require 'resolv'
|
|
|
6
6
|
require 'uri'
|
|
7
7
|
|
|
8
8
|
class SsrfFilter
|
|
9
|
-
def self.prefixlen_from_ipaddr(ipaddr)
|
|
9
|
+
private_class_method def self.prefixlen_from_ipaddr(ipaddr)
|
|
10
10
|
mask_addr = ipaddr.instance_variable_get('@mask_addr')
|
|
11
11
|
raise ArgumentError, 'Invalid mask' if mask_addr.zero?
|
|
12
12
|
|
|
@@ -22,7 +22,19 @@ class SsrfFilter
|
|
|
22
22
|
|
|
23
23
|
length
|
|
24
24
|
end
|
|
25
|
-
|
|
25
|
+
|
|
26
|
+
private_class_method def self.ipv4_from_rfc6052(ipv6_addr, prefix_len)
|
|
27
|
+
n = ipv6_addr.to_i
|
|
28
|
+
ipv4_int = case prefix_len
|
|
29
|
+
when 32 then (n >> 64) & 0xFFFF_FFFF
|
|
30
|
+
when 40 then (((n >> 64) & 0xFFFFFF) << 8) | ((n >> 48) & 0xFF)
|
|
31
|
+
when 48 then (((n >> 64) & 0xFFFF) << 16) | ((n >> 40) & 0xFFFF)
|
|
32
|
+
when 56 then (((n >> 64) & 0xFF) << 24) | ((n >> 32) & 0xFFFFFF)
|
|
33
|
+
when 64 then (n >> 24) & 0xFFFF_FFFF
|
|
34
|
+
when 96 then n & 0xFFFF_FFFF
|
|
35
|
+
end
|
|
36
|
+
::IPAddr.new(ipv4_int, Socket::AF_INET) if ipv4_int
|
|
37
|
+
end
|
|
26
38
|
|
|
27
39
|
# https://en.wikipedia.org/wiki/Reserved_IP_addresses
|
|
28
40
|
IPV4_BLACKLIST = [
|
|
@@ -34,7 +46,6 @@ class SsrfFilter
|
|
|
34
46
|
::IPAddr.new('172.16.0.0/12'), # Private network
|
|
35
47
|
::IPAddr.new('192.0.0.0/24'), # IETF Protocol Assignments
|
|
36
48
|
::IPAddr.new('192.0.2.0/24'), # TEST-NET-1, documentation and examples
|
|
37
|
-
::IPAddr.new('192.88.99.0/24'), # IPv6 to IPv4 relay (includes 2002::/16)
|
|
38
49
|
::IPAddr.new('192.168.0.0/16'), # Private network
|
|
39
50
|
::IPAddr.new('198.18.0.0/15'), # Network benchmark tests
|
|
40
51
|
::IPAddr.new('198.51.100.0/24'), # TEST-NET-2, documentation and examples
|
|
@@ -44,9 +55,11 @@ class SsrfFilter
|
|
|
44
55
|
::IPAddr.new('255.255.255.255') # Broadcast
|
|
45
56
|
].freeze
|
|
46
57
|
|
|
58
|
+
# NAT64 local-use prefix (RFC 8215), uses RFC 6052 /48 encoding (checked at runtime).
|
|
59
|
+
NAT64_LOCAL_PREFIX = ::IPAddr.new('64:ff9b:1::/48').freeze
|
|
60
|
+
|
|
47
61
|
IPV6_BLACKLIST = ([
|
|
48
62
|
::IPAddr.new('::1/128'), # Loopback
|
|
49
|
-
::IPAddr.new('64:ff9b::/96'), # IPv4/IPv6 translation (RFC 6052)
|
|
50
63
|
::IPAddr.new('100::/64'), # Discard prefix (RFC 6666)
|
|
51
64
|
::IPAddr.new('2001::/32'), # Teredo tunneling
|
|
52
65
|
::IPAddr.new('2001:10::/28'), # Deprecated (previously ORCHID)
|
|
@@ -60,10 +73,14 @@ class SsrfFilter
|
|
|
60
73
|
prefixlen = prefixlen_from_ipaddr(ipaddr)
|
|
61
74
|
|
|
62
75
|
# Don't call ipaddr.ipv4_compat because it prints out a deprecation warning on ruby 2.5+
|
|
63
|
-
ipv4_compatible
|
|
64
|
-
ipv4_mapped
|
|
65
|
-
|
|
66
|
-
|
|
76
|
+
ipv4_compatible = IPAddr.new(ipaddr.to_i, Socket::AF_INET6).mask(96 + prefixlen)
|
|
77
|
+
ipv4_mapped = ipaddr.ipv4_mapped.mask(80 + prefixlen)
|
|
78
|
+
# IPv4-translated (RFC 2765): ::ffff:0:x.x.x.x/96+n
|
|
79
|
+
ipv4_translated = IPAddr.new("::ffff:0:#{ipaddr}").mask(96 + prefixlen)
|
|
80
|
+
# NAT64 well-known prefix (RFC 6052): 64:ff9b::x.x.x.x/96+n
|
|
81
|
+
nat64_well_known = IPAddr.new("64:ff9b::#{ipaddr}").mask(96 + prefixlen)
|
|
82
|
+
|
|
83
|
+
[ipv4_compatible, ipv4_mapped, ipv4_translated, nat64_well_known]
|
|
67
84
|
end).freeze
|
|
68
85
|
|
|
69
86
|
DEFAULT_SCHEME_WHITELIST = %w[http https].freeze
|
|
@@ -102,7 +119,7 @@ class SsrfFilter
|
|
|
102
119
|
class CRLFInjection < Error
|
|
103
120
|
end
|
|
104
121
|
|
|
105
|
-
|
|
122
|
+
VERB_MAP.each_key do |method|
|
|
106
123
|
define_singleton_method(method) do |url, options = {}, &block|
|
|
107
124
|
original_url = url
|
|
108
125
|
scheme_whitelist = options.fetch(:scheme_whitelist, DEFAULT_SCHEME_WHITELIST)
|
|
@@ -136,23 +153,32 @@ class SsrfFilter
|
|
|
136
153
|
end
|
|
137
154
|
end
|
|
138
155
|
|
|
139
|
-
def self.unsafe_ip_address?(ip_address)
|
|
156
|
+
private_class_method def self.unsafe_ip_address?(ip_address)
|
|
140
157
|
return true if ipaddr_has_mask?(ip_address)
|
|
141
158
|
|
|
142
159
|
return IPV4_BLACKLIST.any? { |range| range.include?(ip_address) } if ip_address.ipv4?
|
|
143
|
-
|
|
160
|
+
|
|
161
|
+
if ip_address.ipv6?
|
|
162
|
+
return true if IPV6_BLACKLIST.any? { |range| range.include?(ip_address) }
|
|
163
|
+
|
|
164
|
+
# RFC 6052 /48 encoding for NAT64 local-use prefix (RFC 8215): 64:ff9b:1::/48
|
|
165
|
+
# IPv4 is split around u-bits at positions 64-71, so must be decoded at runtime
|
|
166
|
+
if NAT64_LOCAL_PREFIX.dup.include?(ip_address)
|
|
167
|
+
ipv4 = ipv4_from_rfc6052(ip_address, 48)
|
|
168
|
+
return unsafe_ip_address?(ipv4)
|
|
169
|
+
end
|
|
170
|
+
return false
|
|
171
|
+
end
|
|
144
172
|
|
|
145
173
|
true
|
|
146
174
|
end
|
|
147
|
-
private_class_method :unsafe_ip_address?
|
|
148
175
|
|
|
149
|
-
def self.ipaddr_has_mask?(ipaddr)
|
|
176
|
+
private_class_method def self.ipaddr_has_mask?(ipaddr)
|
|
150
177
|
range = ipaddr.to_range
|
|
151
178
|
range.first != range.last
|
|
152
179
|
end
|
|
153
|
-
private_class_method :ipaddr_has_mask?
|
|
154
180
|
|
|
155
|
-
def self.normalized_hostname(uri)
|
|
181
|
+
private_class_method def self.normalized_hostname(uri)
|
|
156
182
|
# Attach port for non-default as per RFC2616
|
|
157
183
|
if (uri.port == 80 && uri.scheme == 'http') ||
|
|
158
184
|
(uri.port == 443 && uri.scheme == 'https')
|
|
@@ -161,9 +187,8 @@ class SsrfFilter
|
|
|
161
187
|
"#{uri.hostname}:#{uri.port}"
|
|
162
188
|
end
|
|
163
189
|
end
|
|
164
|
-
private_class_method :normalized_hostname
|
|
165
190
|
|
|
166
|
-
def self.fetch_once(uri, ip, verb, options, &block)
|
|
191
|
+
private_class_method def self.fetch_once(uri, ip, verb, options, &block)
|
|
167
192
|
if options[:params]
|
|
168
193
|
params = uri.query ? ::URI.decode_www_form(uri.query).to_h : {}
|
|
169
194
|
params.merge!(options[:params])
|
|
@@ -202,9 +227,8 @@ class SsrfFilter
|
|
|
202
227
|
return response, url
|
|
203
228
|
end
|
|
204
229
|
end
|
|
205
|
-
private_class_method :fetch_once
|
|
206
230
|
|
|
207
|
-
def self.validate_request(request)
|
|
231
|
+
private_class_method def self.validate_request(request)
|
|
208
232
|
# RFC822 allows multiline "folded" headers:
|
|
209
233
|
# https://tools.ietf.org/html/rfc822#section-3.1
|
|
210
234
|
# In practice if any user input is ever supplied as a header key/value, they'll get
|
|
@@ -215,5 +239,4 @@ class SsrfFilter
|
|
|
215
239
|
end
|
|
216
240
|
end
|
|
217
241
|
end
|
|
218
|
-
private_class_method :validate_request
|
|
219
242
|
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.4.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:
|
|
11
|
+
date: 2026-03-31 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: base64
|
|
@@ -108,6 +108,20 @@ dependencies:
|
|
|
108
108
|
- - "~>"
|
|
109
109
|
- !ruby/object:Gem::Version
|
|
110
110
|
version: 0.8.0
|
|
111
|
+
- !ruby/object:Gem::Dependency
|
|
112
|
+
name: tsort
|
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
|
114
|
+
requirements:
|
|
115
|
+
- - '='
|
|
116
|
+
- !ruby/object:Gem::Version
|
|
117
|
+
version: 0.1.0
|
|
118
|
+
type: :development
|
|
119
|
+
prerelease: false
|
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
121
|
+
requirements:
|
|
122
|
+
- - '='
|
|
123
|
+
- !ruby/object:Gem::Version
|
|
124
|
+
version: 0.1.0
|
|
111
125
|
- !ruby/object:Gem::Dependency
|
|
112
126
|
name: webmock
|
|
113
127
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -167,7 +181,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
167
181
|
- !ruby/object:Gem::Version
|
|
168
182
|
version: '0'
|
|
169
183
|
requirements: []
|
|
170
|
-
rubygems_version: 3.
|
|
184
|
+
rubygems_version: 3.5.16
|
|
171
185
|
signing_key:
|
|
172
186
|
specification_version: 4
|
|
173
187
|
summary: A gem that makes it easy to prevent server side request forgery (SSRF) attacks
|