Module: Angarium::AddressPolicy
- Defined in:
- lib/angarium/address_policy.rb
Overview
Central SSRF policy. Decides whether a destination IP is permitted for a given endpoint, composing three controls:
1. endpoint.allowed_networks (CIDR list) -> if present, ONLY those are allowed
2. endpoint.allow_private_network -> bypass the private-IP denylist
3. Angarium.config.block_private_ips -> default denylist of private ranges
Class Method Summary collapse
- .cidr_include?(cidr, ip) ⇒ Boolean
-
.host_permitted_for_validation?(host, endpoint) ⇒ Boolean
Resolve host and return true only if EVERY resolved address is allowed.
-
.ip_allowed?(ip, endpoint) ⇒ Boolean
Is a single IP (String or IPAddr) permitted for this endpoint?.
-
.normalize(ip) ⇒ Object
Collapse IPv4-mapped IPv6 (::ffff:x.x.x.x) to native IPv4 so the predicates above see the real address; leave other addresses untouched.
- .private?(ip) ⇒ Boolean
- .resolve(host) ⇒ Object
- .to_ipaddr(value) ⇒ Object
Class Method Details
.cidr_include?(cidr, ip) ⇒ Boolean
69 70 71 72 73 |
# File 'lib/angarium/address_policy.rb', line 69 def cidr_include?(cidr, ip) IPAddr.new(cidr.to_s).include?(ip) rescue IPAddr::InvalidAddressError false end |
.host_permitted_for_validation?(host, endpoint) ⇒ Boolean
Resolve host and return true only if EVERY resolved address is allowed. Unresolvable hosts return [] and this returns true (can't prove disallowed); callers that need strictness at connect time check each resolved IP instead.
40 41 42 |
# File 'lib/angarium/address_policy.rb', line 40 def host_permitted_for_validation?(host, endpoint) resolve(host).all? { |ip| ip_allowed?(ip, endpoint) } end |
.ip_allowed?(ip, endpoint) ⇒ Boolean
Is a single IP (String or IPAddr) permitted for this endpoint?
Two independent gates, both must pass:
Gate A (private denylist): private/loopback/link-local addresses are
blocked unless endpoint.allow_private_network is set. An allowlist entry
does NOT by itself unlock a private address.
Gate B (allowlist): when endpoint.allowed_networks is non-empty, the
address must fall within one of those CIDRs.
21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
# File 'lib/angarium/address_policy.rb', line 21 def ip_allowed?(ip, endpoint) ip = to_ipaddr(ip) return false unless ip if private?(ip) && Angarium.config.block_private_ips return false unless endpoint.allow_private_network end allowlist = Array(endpoint.allowed_networks).reject(&:blank?) unless allowlist.empty? return false unless allowlist.any? { |cidr| cidr_include?(cidr, ip) } end true end |
.normalize(ip) ⇒ Object
Collapse IPv4-mapped IPv6 (::ffff:x.x.x.x) to native IPv4 so the predicates above see the real address; leave other addresses untouched.
63 64 65 66 67 |
# File 'lib/angarium/address_policy.rb', line 63 def normalize(ip) ip.respond_to?(:native) ? ip.native : ip rescue ip end |
.private?(ip) ⇒ Boolean
53 54 55 56 57 58 59 |
# File 'lib/angarium/address_policy.rb', line 53 def private?(ip) ip = normalize(ip) return true if ip.to_i.zero? # 0.0.0.0 / :: (unspecified -> localhost on Linux) ip.loopback? || ip.private? || ip.link_local? || (ip.respond_to?(:unique_local?) && ip.unique_local?) end |
.resolve(host) ⇒ Object
44 45 46 47 48 49 50 51 |
# File 'lib/angarium/address_policy.rb', line 44 def resolve(host) literal = to_ipaddr(host) return [literal] if literal Resolv.getaddresses(host).filter_map { |a| to_ipaddr(a) } rescue [] end |
.to_ipaddr(value) ⇒ Object
75 76 77 78 79 80 |
# File 'lib/angarium/address_policy.rb', line 75 def to_ipaddr(value) ip = value.is_a?(IPAddr) ? value : IPAddr.new(value.to_s) normalize(ip) rescue IPAddr::InvalidAddressError nil end |