Skip to content

Commit 654073c

Browse files
committed
Block IPv6 SSRF bypass via ipv4_compat addresses
Adds ipv4_mapped? and ipv4_compat? checks to PrivateNetworkGuard.private_ip? to block SSRF bypass attempts using IPv6 address formats like: - ::ffff:169.254.169.254 (IPv4-mapped) - ::169.254.169.254 (IPv4-compatible) These formats could previously bypass the link_local? check since Ruby treats them as IPv6 addresses, not IPv4. Ref: HackerOne #3481701
1 parent 53e36a1 commit 654073c

2 files changed

Lines changed: 92 additions & 1 deletion

File tree

lib/restricted_http/private_network_guard.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ def resolve(hostname)
1616

1717
def private_ip?(ip)
1818
IPAddr.new(ip).then do |ipaddr|
19-
ipaddr.private? || ipaddr.loopback? || ipaddr.link_local? || ipaddr.ipv4_mapped? || LOCAL_IP.include?(ipaddr)
19+
ipaddr.private? || ipaddr.loopback? || ipaddr.link_local? || ipaddr.ipv4_mapped? || ipaddr.ipv4_compat? || LOCAL_IP.include?(ipaddr)
2020
end
2121
rescue IPAddr::InvalidAddressError
2222
true
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
require "test_helper"
2+
require "restricted_http/private_network_guard"
3+
4+
class RestrictedHTTP::PrivateNetworkGuardTest < ActiveSupport::TestCase
5+
test "private_ip? returns true for 'This' network (RFC1700)" do
6+
assert_private_ip "0.0.0.0"
7+
assert_private_ip "0.255.255.255"
8+
end
9+
10+
test "private_ip? returns true for loopback addresses" do
11+
assert_private_ip "127.0.0.0"
12+
assert_private_ip "127.0.0.1"
13+
assert_private_ip "127.255.255.255"
14+
end
15+
16+
test "private_ip? returns true for RFC1918 private addresses" do
17+
assert_private_ip "10.0.0.0"
18+
assert_private_ip "10.255.255.255"
19+
assert_private_ip "172.16.0.0"
20+
assert_private_ip "172.31.255.255"
21+
assert_private_ip "192.168.0.0"
22+
assert_private_ip "192.168.255.255"
23+
end
24+
25+
test "private_ip? returns true for link-local addresses" do
26+
assert_private_ip "169.254.0.1"
27+
assert_private_ip "169.254.169.254" # AWS IMDS
28+
assert_private_ip "169.254.255.255"
29+
end
30+
31+
test "private_ip? returns false for public addresses" do
32+
assert_not RestrictedHTTP::PrivateNetworkGuard.private_ip?("93.184.216.34")
33+
assert_not RestrictedHTTP::PrivateNetworkGuard.private_ip?("8.8.8.8")
34+
end
35+
36+
# IPv6 address format tests (SSRF bypass prevention)
37+
38+
test "private_ip? returns true for IPv4-mapped IPv6 addresses with private IPs" do
39+
assert_private_ip "::ffff:192.168.1.1"
40+
assert_private_ip "::ffff:10.0.0.1"
41+
assert_private_ip "::ffff:172.16.0.1"
42+
end
43+
44+
test "private_ip? returns true for IPv4-mapped IPv6 addresses with link-local IPs" do
45+
assert_private_ip "::ffff:169.254.169.254" # AWS metadata via mapped format
46+
end
47+
48+
test "private_ip? returns true for IPv4-mapped IPv6 addresses even with public IPs" do
49+
# Block all ipv4_mapped? since DNS never returns this format legitimately
50+
assert_private_ip "::ffff:93.184.216.34"
51+
end
52+
53+
test "private_ip? returns true for IPv4-compatible IPv6 addresses with private IPs" do
54+
assert_private_ip "::192.168.1.1"
55+
assert_private_ip "::10.0.0.1"
56+
end
57+
58+
test "private_ip? returns true for IPv4-compatible IPv6 addresses with link-local IPs" do
59+
assert_private_ip "::169.254.169.254" # AWS metadata via compat format - the reported bypass
60+
end
61+
62+
test "private_ip? returns true for IPv4-compatible IPv6 addresses even with public IPs" do
63+
# Block all ipv4_compat? since DNS never returns this format legitimately
64+
assert_private_ip "::93.184.216.34"
65+
end
66+
67+
test "private_ip? returns true for invalid addresses" do
68+
assert RestrictedHTTP::PrivateNetworkGuard.private_ip?("not-an-ip")
69+
assert RestrictedHTTP::PrivateNetworkGuard.private_ip?("")
70+
end
71+
72+
test "resolve raises Violation for private hostname" do
73+
Resolv.stub :getaddress, "192.168.1.1" do
74+
assert_raises RestrictedHTTP::Violation do
75+
RestrictedHTTP::PrivateNetworkGuard.resolve("private.example.com")
76+
end
77+
end
78+
end
79+
80+
test "resolve returns IP for public hostname" do
81+
Resolv.stub :getaddress, "93.184.216.34" do
82+
assert_equal "93.184.216.34", RestrictedHTTP::PrivateNetworkGuard.resolve("example.com")
83+
end
84+
end
85+
86+
private
87+
def assert_private_ip(address)
88+
assert RestrictedHTTP::PrivateNetworkGuard.private_ip?(address),
89+
"Expected #{address} to be classified as private"
90+
end
91+
end

0 commit comments

Comments
 (0)