diff --git a/lib/restricted_http/private_network_guard.rb b/lib/restricted_http/private_network_guard.rb index 193deeb..f5483ff 100644 --- a/lib/restricted_http/private_network_guard.rb +++ b/lib/restricted_http/private_network_guard.rb @@ -8,6 +8,14 @@ module RestrictedHTTP LOCAL_IP = IPAddr.new("0.0.0.0/8") # "This" network + # IPv6 transition/encapsulation and shared-address ranges that can smuggle + # requests toward internal networks and must never be reachable. + DISALLOWED_RANGES = [ + IPAddr.new("64:ff9b::/96"), # NAT64 (RFC 6052) — embeds IPv4 in IPv6 + IPAddr.new("2002::/16"), # 6to4 (RFC 3056) — embeds IPv4 in IPv6 + IPAddr.new("100.64.0.0/10") # CGNAT / shared address space (RFC 6598) + ] + def resolve(hostname) Resolv.getaddress(hostname).tap do |ip| raise Violation.new("Attempt to access private IP via #{hostname}") if ip && private_ip?(ip) @@ -16,7 +24,7 @@ module RestrictedHTTP def private_ip?(ip) IPAddr.new(ip).then do |ipaddr| - ipaddr.private? || ipaddr.loopback? || ipaddr.link_local? || ipaddr.ipv4_mapped? || ipaddr.ipv4_compat? || LOCAL_IP.include?(ipaddr) + ipaddr.private? || ipaddr.loopback? || ipaddr.link_local? || ipaddr.ipv4_mapped? || ipaddr.ipv4_compat? || LOCAL_IP.include?(ipaddr) || DISALLOWED_RANGES.any? { |range| range.include?(ipaddr) } end rescue IPAddr::InvalidAddressError true diff --git a/test/lib/restricted_http/private_network_guard_test.rb b/test/lib/restricted_http/private_network_guard_test.rb index 816a648..7f39338 100644 --- a/test/lib/restricted_http/private_network_guard_test.rb +++ b/test/lib/restricted_http/private_network_guard_test.rb @@ -64,6 +64,26 @@ class RestrictedHTTP::PrivateNetworkGuardTest < ActiveSupport::TestCase assert_private_ip "::93.184.216.34" end + # IPv6/encapsulation and shared-address ranges (SSRF bypass prevention) + + test "private_ip? returns true for NAT64 addresses" do + assert_private_ip "64:ff9b::a00:1" # NAT64-embedded 10.0.0.1 + end + + test "private_ip? returns true for 6to4 addresses" do + assert_private_ip "2002::1" + assert_private_ip "2002:0a00:0001::" # 6to4-embedded 10.0.0.1 + end + + test "private_ip? returns true for CGNAT / shared address space" do + assert_private_ip "100.64.0.1" + assert_private_ip "100.127.255.255" + end + + test "private_ip? returns false for public addresses outside the new ranges" do + assert_not RestrictedHTTP::PrivateNetworkGuard.private_ip?("93.184.216.34") + end + test "private_ip? returns true for invalid addresses" do assert RestrictedHTTP::PrivateNetworkGuard.private_ip?("not-an-ip") assert RestrictedHTTP::PrivateNetworkGuard.private_ip?("")