SECURITY: SSRF protection bypass with IPv4-mapped IPv6 addresses

As part of this commit, we've also expanded our list of private IP
ranges based on
https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml
and https://www.iana.org/assignments/iana-ipv6-special-registry/iana-ipv6-special-registry.xhtml
This commit is contained in:
Alan Guo Xiang Tan 2023-03-06 09:20:12 +08:00 committed by Blake Erickson
parent 3c49c4ee35
commit 87032e87ea
2 changed files with 56 additions and 18 deletions

View File

@ -7,18 +7,47 @@ class FinalDestination
class LookupFailedError < SocketError
end
def self.standard_private_ranges
@private_ranges ||= [
# This is a list of private IPv4 IP ranges that are not allowed to be globally reachable as given by
# https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml.
PRIVATE_IPV4_RANGES = [
IPAddr.new("0.0.0.0/8"),
IPAddr.new("127.0.0.1"),
IPAddr.new("172.16.0.0/12"),
IPAddr.new("192.168.0.0/16"),
IPAddr.new("10.0.0.0/8"),
IPAddr.new("::1"),
IPAddr.new("100.64.0.0/10"),
IPAddr.new("127.0.0.0/8"),
IPAddr.new("169.254.0.0/16"),
IPAddr.new("172.16.0.0/12"),
IPAddr.new("192.0.0.0/24"),
IPAddr.new("192.0.0.0/29"),
IPAddr.new("192.0.0.8/32"),
IPAddr.new("192.0.0.170/32"),
IPAddr.new("192.0.0.171/32"),
IPAddr.new("192.0.2.0/24"),
IPAddr.new("192.168.0.0/16"),
IPAddr.new("192.175.48.0/24"),
IPAddr.new("198.18.0.0/15"),
IPAddr.new("198.51.100.0/24"),
IPAddr.new("203.0.113.0/24"),
IPAddr.new("240.0.0.0/4"),
IPAddr.new("255.255.255.255/32"),
]
# This is a list of private IPv6 IP ranges that are not allowed to be globally reachable as given by
# https://www.iana.org/assignments/iana-ipv6-special-registry/iana-ipv6-special-registry.xhtml.
#
# ::ffff:0:0/96 is excluded from the list because it is used for IPv4-mapped IPv6 addresses which is something we want to allow.
PRIVATE_IPV6_RANGES = [
IPAddr.new("::1/128"),
IPAddr.new("::/128"),
IPAddr.new("64:ff9b:1::/48"),
IPAddr.new("100::/64"),
IPAddr.new("2001::/23"),
IPAddr.new("2001:2::/48"),
IPAddr.new("2001:db8::/32"),
IPAddr.new("fc00::/7"),
IPAddr.new("fe80::/10"),
]
end
PRIVATE_IP_RANGES = PRIVATE_IPV4_RANGES + PRIVATE_IPV6_RANGES
def self.blocked_ip_blocks
SiteSetting
@ -54,10 +83,9 @@ class FinalDestination
def self.ip_allowed?(ip)
ip = ip.is_a?(IPAddr) ? ip : IPAddr.new(ip)
ip = ip.native
if ip_in_ranges?(ip, blocked_ip_blocks) || ip_in_ranges?(ip, standard_private_ranges)
return false
end
return false if ip_in_ranges?(ip, blocked_ip_blocks) || ip_in_ranges?(ip, PRIVATE_IP_RANGES)
true
end

View File

@ -43,9 +43,19 @@ describe FinalDestination::SSRFDetector do
expect(subject.ip_allowed?("9001:82f3:8873::3")).to eq(false)
end
it "returns false for standard internal IPs" do
expect(subject.ip_allowed?("172.31.100.31")).to eq(false)
expect(subject.ip_allowed?("fd02:77fa:ffea::f")).to eq(false)
%w[0.0.0.0 10.0.0.0 127.0.0.0 172.31.100.31 255.255.255.255 ::1 ::].each do |internal_ip|
it "returns false for '#{internal_ip}'" do
expect(subject.ip_allowed?(internal_ip)).to eq(false)
end
end
it "returns false for private IPv4-mapped IPv6 addresses" do
expect(subject.ip_allowed?("::ffff:172.31.100.31")).to eq(false)
expect(subject.ip_allowed?("::ffff:0.0.0.0")).to eq(false)
end
it "returns true for public IPv4-mapped IPv6 addresses" do
expect(subject.ip_allowed?("::ffff:52.52.167.244")).to eq(true)
end
end