mirror of
https://github.com/basecamp/once-campfire.git
synced 2026-08-23 23:41:06 +09:00
c860b51109
* Take the SSRF address policy from surfguard instead of keeping our own copy Four other apps carried this same classification and the five had drifted into four different ideas of what "internal" means. It now comes from the surfguard gem, which is their union. resolve and Violation keep their shapes, so the opengraph callers are unchanged. Two verdicts change. SIIT (::ffff:0:0:0/96) is now recognised. It 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 for ::ffff:0:a9fe:a9fe, so it fell through to the IPv6 branch unrecognised and reached the metadata endpoint. Note the extra group: ::ffff:0:0:0/96 is not the IPv4-mapped ::ffff:0:0/96 the guard already refused, and the two do not overlap. The RFC 8215 local-use NAT64 block is now refused whole rather than decoded. Reading its low 32 bits as an embedded IPv4 is only correct for a /96 Pref64; the block can host any length from /32 to /96 and the position is not recoverable from the address alone (RFC 6052 2.2), so the decode reads the wrong octets. It is never globally routed, so refusing it costs nothing. The well-known /96 is still decoded and re-checked, so DNS64 for public sites on IPv6-only hosts keeps working. Resolution moves from Resolv.getaddress to Resolv.getaddresses, so the guard sees every address a host answers with rather than only the first. * Distinguish a DNS lookup failure from a private-IP block in the guard Advance the surfguard pin so resolve_public_ips raises Unresolvable when a host resolves to nothing and returns an empty list only when it resolves to a blocked address. The shim lets Unresolvable propagate as a lookup failure -- matching the old Resolv.getaddress behavior -- and reserves Violation for a resolved-but-blocked address, so a transient DNS miss is no longer reported as an SSRF attempt.
156 lines
6.2 KiB
Ruby
156 lines
6.2 KiB
Ruby
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
|