Module: Iron::SsrfProtection

Extended by:
SsrfProtection
Included in:
SsrfProtection
Defined in:
app/models/iron/ssrf_protection.rb

Constant Summary collapse

DNS_RESOLUTION_TIMEOUT =
2
GLOBAL_UNICAST_V6 =

2000::/3 is the only IPv6 block IANA has delegated as global unicast, so anything outside it (unspecified, loopback, link-local, ULA, multicast, translation, discard) is non-global by construction. Within it, a few sub-ranges are still reserved and must be denied explicitly.

IPAddr.new("2000::/3")
DISALLOWED_RANGES =
[
  IPAddr.new("0.0.0.0/8"),       # "This" network / unspecified (RFC1122)
  IPAddr.new("100.64.0.0/10"),   # Carrier-grade NAT (RFC6598)
  IPAddr.new("192.0.0.0/24"),    # IETF protocol assignments (RFC6890)
  IPAddr.new("192.0.2.0/24"),    # Documentation TEST-NET-1 (RFC5737)
  IPAddr.new("198.18.0.0/15"),   # Benchmark testing (RFC2544)
  IPAddr.new("198.51.100.0/24"), # Documentation TEST-NET-2 (RFC5737)
  IPAddr.new("203.0.113.0/24"),  # Documentation TEST-NET-3 (RFC5737)
  IPAddr.new("192.88.99.0/24"),  # 6to4 relay anycast (RFC7526)
  IPAddr.new("224.0.0.0/4"),     # IPv4 multicast (RFC5771)
  IPAddr.new("240.0.0.0/4"),     # Reserved (RFC1112)
  IPAddr.new("2001::/23"),       # IETF protocol assignments — non-global (Teredo, ORCHID, benchmarking)
  IPAddr.new("2001:db8::/32"),   # Documentation (RFC3849)
  IPAddr.new("2002::/16"),       # 6to4 (RFC3056) — non-global
  IPAddr.new("3fff::/20")        # Documentation (RFC9637)
].freeze

Instance Method Summary collapse

Instance Method Details

#blocked?(ip) ⇒ Boolean

Returns:

  • (Boolean)


46
47
48
49
50
51
52
53
54
55
56
57
58
59
# File 'app/models/iron/ssrf_protection.rb', line 46

def blocked?(ip)
  ip = IPAddr.new(ip.to_s) unless ip.is_a?(IPAddr)

  return true if ip.ipv6? && !GLOBAL_UNICAST_V6.include?(ip)

  ip.private? ||
    ip.loopback? ||
    ip.link_local? ||
    ip.ipv4_mapped? ||
    ip.ipv4_compat? ||
    DISALLOWED_RANGES.any? { |range| range.include?(ip) }
rescue IPAddr::Error
  true
end

#public_addresses(hostname) ⇒ Object

Resolves a hostname and returns its addresses only when every one of them is public, so a caller can connect to a vetted IP without re-resolving.



39
40
41
42
43
44
# File 'app/models/iron/ssrf_protection.rb', line 39

def public_addresses(hostname)
  addresses = resolve(hostname)
  return [] if addresses.empty? || addresses.any? { |ip| blocked?(ip) }

  addresses.map(&:to_s)
end

#public_host?(hostname) ⇒ Boolean

Returns:

  • (Boolean)


33
34
35
# File 'app/models/iron/ssrf_protection.rb', line 33

def public_host?(hostname)
  public_addresses(hostname).any?
end