Module: OpenLoam::OutboundUrl

Defined in:
lib/open_loam/outbound_url.rb

Overview

Guard for URLs a TENANT supplies and the SERVER then fetches — webhook endpoints and the SSO issuer. Without it the tenant names an address only the server can reach (169.254.169.254, loopback, a private subnet) and gets the result back from inside the perimeter.

uri, address = OpenLoam::OutboundUrl.resolve!(endpoint.url)
http = Net::HTTP.new(uri.host, uri.port)
http.ipaddr = address   # connect to the address that was CHECKED

The pin is the load-bearing half: a host that resolves publicly during the check and privately a moment later (DNS rebinding) beats a name-only check.

Defined Under Namespace

Classes: BlockedError

Constant Summary collapse

SCHEMES =
%w[http https].freeze
BLOCKED =

Loopback, link-local (incl. the 169.254.169.254 cloud metadata service), RFC1918, carrier-grade NAT, and the IPv6 equivalents.

[
  "0.0.0.0/8", "10.0.0.0/8", "100.64.0.0/10", "127.0.0.0/8", "169.254.0.0/16",
  "172.16.0.0/12", "192.0.0.0/24", "192.168.0.0/16", "198.18.0.0/15",
  "224.0.0.0/4", "240.0.0.0/4",
  "::1/128", "::/128", "fc00::/7", "fe80::/10", "ff00::/8"
].map { |range| IPAddr.new(range) }.freeze

Class Method Summary collapse

Class Method Details

.address_for!(host) ⇒ Object

Every address the host resolves to must be allowed — a name with one public and one private A record is still an internal reach.

Raises:



69
70
71
72
73
74
75
76
77
# File 'lib/open_loam/outbound_url.rb', line 69

def address_for!(host)
  addresses = resolve_all(host)
  raise BlockedError, "#{host} does not resolve" if addresses.empty?

  addresses.each do |address|
    raise BlockedError, "#{host} resolves to the non-public address #{address}" if blocked?(address)
  end
  addresses.first
end

.blocked?(address) ⇒ Boolean

Returns:

  • (Boolean)


93
94
95
96
97
98
# File 'lib/open_loam/outbound_url.rb', line 93

def blocked?(address)
  ip = IPAddr.new(address.to_s)
  BLOCKED.any? { |range| range.include?(ip) }
rescue IPAddr::Error
  true # unparseable is not provably public
end

.literal_address(host) ⇒ Object



87
88
89
90
91
# File 'lib/open_loam/outbound_url.rb', line 87

def literal_address(host)
  IPAddr.new(host.to_s)
rescue IPAddr::Error
  nil
end

.parse!(url, require_https: false) ⇒ Object

Raises:



50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# File 'lib/open_loam/outbound_url.rb', line 50

def parse!(url, require_https: false)
  uri = begin
    URI.parse(url.to_s)
  rescue URI::InvalidURIError => error
    raise BlockedError, "not a valid URL: #{error.message}"
  end

  raise BlockedError, "#{uri.scheme.inspect} is not an allowed scheme" unless SCHEMES.include?(uri.scheme)
  raise BlockedError, "https is required" if require_https && uri.scheme != "https"
  raise BlockedError, "no host in #{url.inspect}" if uri.host.blank?
  # Credentials in the URL are a redirect/parsing-confusion trick more often
  # than a real need, and nothing here has a use for them.
  raise BlockedError, "credentials in the URL are not allowed" if uri.userinfo.present?

  uri
end

.resolve!(url, require_https: false) ⇒ Object

Shape, DNS, and the address to connect to — call this at FETCH time.



45
46
47
48
# File 'lib/open_loam/outbound_url.rb', line 45

def resolve!(url, require_https: false)
  uri = validate!(url, require_https: require_https)
  [ uri, address_for!(uri.host) ]
end

.resolve_all(host) ⇒ Object



79
80
81
82
83
84
85
# File 'lib/open_loam/outbound_url.rb', line 79

def resolve_all(host)
  return [ host ] if literal_address(host)

  Resolv.getaddresses(host)
rescue Resolv::ResolvError
  []
end

.validate!(url, require_https: false) ⇒ Object

Shape only — no DNS. Model validations call this: saving must not depend on the network, and a save-time lookup proves nothing resolve! doesn't re-prove.



35
36
37
38
39
40
41
42
# File 'lib/open_loam/outbound_url.rb', line 35

def validate!(url, require_https: false)
  uri = parse!(url, require_https: require_https)
  if literal_address(uri.host) && blocked?(uri.host)
    raise BlockedError, "#{uri.host} is not a public address"
  end

  uri
end