require "test_helper" require "restricted_http/private_network_guard" class RestrictedHTTP::PrivateNetworkGuardTest < ActiveSupport::TestCase test "private_ip? returns true for 'This' network (RFC1700)" do assert_private_ip "0.0.0.0" assert_private_ip "0.255.255.255" end test "private_ip? returns true for loopback addresses" do assert_private_ip "127.0.0.0" assert_private_ip "127.0.0.1" assert_private_ip "127.255.255.255" end test "private_ip? returns true for RFC1918 private addresses" do assert_private_ip "10.0.0.0" assert_private_ip "10.255.255.255" assert_private_ip "172.16.0.0" assert_private_ip "172.31.255.255" assert_private_ip "192.168.0.0" assert_private_ip "192.168.255.255" end test "private_ip? returns true for link-local addresses" do assert_private_ip "169.254.0.1" assert_private_ip "169.254.169.254" # AWS IMDS assert_private_ip "169.254.255.255" end test "private_ip? returns false for public addresses" do assert_not RestrictedHTTP::PrivateNetworkGuard.private_ip?("93.184.216.34") assert_not RestrictedHTTP::PrivateNetworkGuard.private_ip?("8.8.8.8") end # IPv6 address format tests (SSRF bypass prevention) test "private_ip? returns true for IPv4-mapped IPv6 addresses with private IPs" do assert_private_ip "::ffff:192.168.1.1" assert_private_ip "::ffff:10.0.0.1" assert_private_ip "::ffff:172.16.0.1" end test "private_ip? returns true for IPv4-mapped IPv6 addresses with link-local IPs" do assert_private_ip "::ffff:169.254.169.254" # AWS metadata via mapped format end test "private_ip? returns true for IPv4-mapped IPv6 addresses even with public IPs" do # Block all ipv4_mapped? since DNS never returns this format legitimately assert_private_ip "::ffff:93.184.216.34" end test "private_ip? returns true for IPv4-compatible IPv6 addresses with private IPs" do assert_private_ip "::192.168.1.1" assert_private_ip "::10.0.0.1" end test "private_ip? returns true for IPv4-compatible IPv6 addresses with link-local IPs" do assert_private_ip "::169.254.169.254" # AWS metadata via compat format - the reported bypass end test "private_ip? returns true for IPv4-compatible IPv6 addresses even with public IPs" do # Block all ipv4_compat? since DNS never returns this format legitimately assert_private_ip "::93.184.216.34" end test "private_ip? returns true for carrier-grade NAT addresses (RFC6598)" do assert_private_ip "100.64.0.1" assert_private_ip "100.127.255.255" end test "private_ip? returns true for NAT64 addresses embedding a private IPv4" do assert_private_ip "64:ff9b::a9fe:a9fe" # NAT64 -> 169.254.169.254 (AWS metadata) assert_private_ip "64:ff9b::a00:5" # NAT64 -> 10.0.0.5 end # The local-use block is refused whole rather than decoded. A Pref64 inside it # can be any length from /32 to /96 and the position is not recoverable from # the address alone (RFC 6052 ยง2.2), so reading the low 32 bits reads the # wrong octets -- 64:ff9b:1::808:808 only looks like 8.8.8.8 under a /96 # reading. The block is never globally routed, so nothing legitimate is lost. test "private_ip? returns true for the whole local-use NAT64 block (RFC8215)" do assert_private_ip "64:ff9b:1::a00:1" # reads as 10.0.0.1 under a /96 assert_private_ip "64:ff9b:1::808:808" # reads as 8.8.8.8 under a /96 assert_private_ip "64:ff9b:1:ffff::1" end # SIIT is the third way an IPv4 address rides inside an IPv6 one and the only # one Ruby has no predicate for: ipv4_mapped?, ipv4_compat?, private?, # loopback? and link_local? are all false here. Note the extra group -- # ::ffff:0:0:0/96 is not the IPv4-mapped ::ffff:0:0/96, and they do not # overlap -- so this reached the metadata endpoint unrecognised. test "private_ip? returns true for SIIT IPv4-translated addresses (RFC2765)" do assert_private_ip "::ffff:0:169.254.169.254" assert_private_ip "::ffff:0:a9fe:a9fe" # the same address, hex spelling assert_private_ip "::ffff:0:127.0.0.1" assert_private_ip "::ffff:0:192.168.0.1" end test "private_ip? returns false for NAT64 addresses embedding a public IPv4" do # DNS64 legitimately synthesizes these for public sites on IPv6-only hosts. assert_not RestrictedHTTP::PrivateNetworkGuard.private_ip?("64:ff9b::808:808") # -> 8.8.8.8 end test "private_ip? returns true for 6to4 and Teredo transition addresses" do assert_private_ip "2002:a9fe:a9fe::" # 6to4 embedding 169.254.169.254 assert_private_ip "2001::1" # Teredo end test "private_ip? returns true for IPv6 loopback, ULA, and link-local" do assert_private_ip "::1" assert_private_ip "fd00:ec2::254" # AWS IMDSv6 (ULA) assert_private_ip "fe80::1" end test "private_ip? returns true for IPv6 multicast, documentation, and benchmarking ranges" do assert_private_ip "ff02::1" assert_private_ip "2001:db8::1" assert_private_ip "2001:2::1" # benchmarking (RFC5180), matches 198.18.0.0/15 end test "private_ip? returns false for public IPv6 addresses" do assert_not RestrictedHTTP::PrivateNetworkGuard.private_ip?("2606:4700:4700::1111") end test "private_ip? returns true for invalid addresses" do assert RestrictedHTTP::PrivateNetworkGuard.private_ip?("not-an-ip") assert RestrictedHTTP::PrivateNetworkGuard.private_ip?("") end test "resolve raises Violation for private hostname" do Resolv.stubs(:getaddresses).returns([ "192.168.1.1" ]) assert_raises RestrictedHTTP::Violation do RestrictedHTTP::PrivateNetworkGuard.resolve("private.example.com") end end test "resolve returns IP for public hostname" do Resolv.stubs(:getaddresses).returns([ "93.184.216.34" ]) assert_equal "93.184.216.34", RestrictedHTTP::PrivateNetworkGuard.resolve("example.com") end test "resolve raises Unresolvable, not Violation, when the host resolves to nothing" do Resolv.stubs(:getaddresses).returns([]) assert_raises Surfguard::Unresolvable do RestrictedHTTP::PrivateNetworkGuard.resolve("nxdomain.example.com") end end private def assert_private_ip(address) assert RestrictedHTTP::PrivateNetworkGuard.private_ip?(address), "Expected #{address} to be classified as private" end end