mirror of
https://github.com/basecamp/once-campfire.git
synced 2026-08-07 15:28:45 +09:00
c0cade51a1
The guard uses IPAddr but relied on something else loading it first; require it explicitly. And the rebinding tests lost their port assertion when the matchers were loosened for newer Net::HTTP keyword args, so check the port alongside the IP again.
77 lines
2.6 KiB
Ruby
77 lines
2.6 KiB
Ruby
require "ipaddr"
|
|
require "resolv"
|
|
|
|
module RestrictedHTTP
|
|
class Violation < StandardError; end
|
|
|
|
module PrivateNetworkGuard
|
|
extend self
|
|
|
|
# IPv4 special-use ranges (RFC 5735/6890) not already covered by the
|
|
# private?/loopback?/link_local? predicates in #disallowed_ipv4?.
|
|
DISALLOWED_IPV4 = %w[
|
|
0.0.0.0/8 100.64.0.0/10 192.0.0.0/24 192.0.2.0/24 192.88.99.0/24
|
|
198.18.0.0/15 198.51.100.0/24 203.0.113.0/24 224.0.0.0/4 240.0.0.0/4
|
|
].map { |cidr| IPAddr.new(cidr) }.freeze
|
|
|
|
# IPv6 special-use ranges not caught by the predicates. 6to4 (2002::/16) and
|
|
# Teredo (2001::/32) are deprecated transition mechanisms with no legitimate
|
|
# fetch target, so they are blocked outright. ULA (fc00::/7, incl. the AWS
|
|
# IMDSv6 address fd00:ec2::254), link-local, and loopback are covered by the
|
|
# predicates in #disallowed_ipv6?.
|
|
DISALLOWED_IPV6 = %w[
|
|
::/128 100::/64 2001::/32 2001:2::/48 2001:db8::/32 2002::/16
|
|
fec0::/10 ff00::/8
|
|
].map { |cidr| IPAddr.new(cidr) }.freeze
|
|
|
|
# NAT64 prefixes: the well-known prefix (RFC 6052/6146) and the local-use
|
|
# prefix (RFC 8215). An address here embeds an IPv4 target in its low 32
|
|
# bits; we extract it and re-check against the IPv4 rules so NAT64 to a
|
|
# public address still resolves while NAT64 to an internal address is
|
|
# blocked.
|
|
NAT64_PREFIXES = [
|
|
IPAddr.new("64:ff9b::/96"),
|
|
IPAddr.new("64:ff9b:1::/48")
|
|
].freeze
|
|
|
|
def resolve(hostname)
|
|
Resolv.getaddress(hostname).tap do |ip|
|
|
raise Violation.new("Attempt to access private IP via #{hostname}") if ip && private_ip?(ip)
|
|
end
|
|
end
|
|
|
|
def private_ip?(ip)
|
|
ipaddr = IPAddr.new(ip)
|
|
|
|
# DNS never legitimately returns these embedded forms, so block them all
|
|
# regardless of the address they wrap.
|
|
if ipaddr.ipv4_mapped? || ipaddr.ipv4_compat?
|
|
true
|
|
elsif ipaddr.ipv4?
|
|
disallowed_ipv4?(ipaddr)
|
|
elsif NAT64_PREFIXES.any? { |prefix| prefix.include?(ipaddr) }
|
|
disallowed_ipv4?(embedded_ipv4(ipaddr))
|
|
else
|
|
disallowed_ipv6?(ipaddr)
|
|
end
|
|
rescue IPAddr::InvalidAddressError
|
|
true
|
|
end
|
|
|
|
private
|
|
def disallowed_ipv4?(ipaddr)
|
|
ipaddr.private? || ipaddr.loopback? || ipaddr.link_local? ||
|
|
DISALLOWED_IPV4.any? { |range| range.include?(ipaddr) }
|
|
end
|
|
|
|
def disallowed_ipv6?(ipaddr)
|
|
ipaddr.private? || ipaddr.loopback? || ipaddr.link_local? ||
|
|
DISALLOWED_IPV6.any? { |range| range.include?(ipaddr) }
|
|
end
|
|
|
|
def embedded_ipv4(ipaddr)
|
|
IPAddr.new([ ipaddr.to_i & 0xffffffff ].pack("N").unpack("C4").join("."))
|
|
end
|
|
end
|
|
end
|