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
-
.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.
- .blocked?(address) ⇒ Boolean
- .literal_address(host) ⇒ Object
- .parse!(url, require_https: false) ⇒ Object
-
.resolve!(url, require_https: false) ⇒ Object
Shape, DNS, and the address to connect to — call this at FETCH time.
- .resolve_all(host) ⇒ Object
-
.validate!(url, require_https: false) ⇒ Object
Shape only — no DNS.
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.
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
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
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.}" 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 |