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

Class Method Details

.cidr_include?(cidr, ip) ⇒ Boolean

Returns:

  • (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.

Returns:

  • (Boolean)


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.

Returns:

  • (Boolean)


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

Returns:

  • (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