Block IPv4-compatible IPv6 addresses in SSRF protection (#2273)
The SSRF filter checked ipv4_mapped? but not ipv4_compat?, allowing addresses like ::169.254.169.254 to bypass the link-local check and reach cloud metadata endpoints. Changes: - Add ipv4_compat? check to block deprecated IPv4-compatible format - Rename private_address? to blocked_address? (more accurate - method blocks more than just RFC 1918 private ranges) - Add IPv6 test coverage for both mapped and compat formats Both ipv4_mapped and ipv4_compat formats are blocked entirely as defense-in-depth: DNS never returns these formats, so they only appear in attack scenarios. HackerOne: #3481701
This commit is contained in:
@@ -16,13 +16,19 @@ module SsrfProtection
|
||||
|
||||
def resolve_public_ip(hostname)
|
||||
ip_addresses = resolve_dns(hostname)
|
||||
public_ips = ip_addresses.reject { |ip| private_address?(ip) }
|
||||
public_ips = ip_addresses.reject { |ip| blocked_address?(ip) }
|
||||
public_ips.sort_by { |ipaddr| ipaddr.ipv4? ? 0 : 1 }.first&.to_s
|
||||
end
|
||||
|
||||
def private_address?(ip)
|
||||
def blocked_address?(ip)
|
||||
ip = IPAddr.new(ip.to_s) unless ip.is_a?(IPAddr)
|
||||
ip.private? || ip.loopback? || ip.link_local? || ip.ipv4_mapped? || in_disallowed_range?(ip)
|
||||
|
||||
ip.private? ||
|
||||
ip.loopback? ||
|
||||
ip.link_local? ||
|
||||
ip.ipv4_mapped? ||
|
||||
ip.ipv4_compat? ||
|
||||
in_disallowed_range?(ip)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -51,6 +51,38 @@ class SsrfProtectionTest < ActiveSupport::TestCase
|
||||
assert_equal "93.184.216.34", SsrfProtection.resolve_public_ip("multi.example.com")
|
||||
end
|
||||
|
||||
# IPv6 address format tests (SSRF bypass prevention)
|
||||
|
||||
test "blocks IPv4-mapped IPv6 addresses with private IPs" do
|
||||
stub_dns_resolution("::ffff:192.168.1.1")
|
||||
assert_nil SsrfProtection.resolve_public_ip("mapped-private.example.com")
|
||||
end
|
||||
|
||||
test "blocks IPv4-mapped IPv6 addresses with link-local IPs" do
|
||||
stub_dns_resolution("::ffff:169.254.169.254")
|
||||
assert_nil SsrfProtection.resolve_public_ip("mapped-metadata.example.com")
|
||||
end
|
||||
|
||||
test "blocks IPv4-mapped IPv6 addresses even with public IPs" do
|
||||
stub_dns_resolution("::ffff:93.184.216.34")
|
||||
assert_nil SsrfProtection.resolve_public_ip("mapped-public.example.com")
|
||||
end
|
||||
|
||||
test "blocks IPv4-compatible IPv6 addresses with private IPs" do
|
||||
stub_dns_resolution("::192.168.1.1")
|
||||
assert_nil SsrfProtection.resolve_public_ip("compat-private.example.com")
|
||||
end
|
||||
|
||||
test "blocks IPv4-compatible IPv6 addresses with link-local IPs" do
|
||||
stub_dns_resolution("::169.254.169.254")
|
||||
assert_nil SsrfProtection.resolve_public_ip("compat-metadata.example.com")
|
||||
end
|
||||
|
||||
test "blocks IPv4-compatible IPv6 addresses even with public IPs" do
|
||||
stub_dns_resolution("::93.184.216.34")
|
||||
assert_nil SsrfProtection.resolve_public_ip("compat-public.example.com")
|
||||
end
|
||||
|
||||
private
|
||||
def stub_dns_resolution(*ips)
|
||||
dns_mock = mock("dns")
|
||||
|
||||
Reference in New Issue
Block a user