Class: Hitch::ClientIdMetadata::Fetcher
- Inherits:
-
Object
- Object
- Hitch::ClientIdMetadata::Fetcher
- Defined in:
- app/models/hitch/client_id_metadata/fetcher.rb
Overview
The SSRF-hardened document fetch: resolve once, vet every address, pin the connection to the vetted address, cap time and size, follow nothing, and accept only a document that names itself with the exact URL it came from.
Constant Summary collapse
- BLOCKED_IPV4 =
Non-public destinations. A CIMD URL resolving into any of these is someone using the authorization server as a proxy into a network they cannot otherwise reach — cloud metadata endpoints (169.254.169.254), internal services, the host itself.
[ "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.0.2.0/24", "192.88.99.0/24", "192.168.0.0/16", "198.18.0.0/15", "198.51.100.0/24", "203.0.113.0/24", "224.0.0.0/4", "240.0.0.0/4", "255.255.255.255/32" ].map { |r| IPAddr.new(r) }.freeze
- GLOBAL_UNICAST_IPV6 =
IPv6 is an ALLOWLIST, not a denylist. A denylist cannot be made complete here: RFC 8215 reserves 64:ff9b:1::/48 for network-specific NAT64 prefixes, and 6to4 and Teredo embed an arbitrary IPv4 destination that a denylist would have to decode to evaluate. So only global unicast is allowed through, minus the special-purpose blocks carved out of it. Everything else — loopback, link-local, unique-local, IPv4-mapped, IPv4-compatible, site-local, NAT64, multicast — falls outside 2000::/3 and is refused by default.
IPAddr.new("2000::/3")
- EXCLUDED_IPV6 =
[ "2001::/32", # Teredo — tunnels to an arbitrary IPv4 endpoint "2001:10::/28", # ORCHID (deprecated) "2001:20::/28", # ORCHIDv2 "2001:2::/48", # benchmarking — the v6 counterpart of 198.18.0.0/15 "2001:db8::/32", # documentation "2002::/16", # 6to4 — embeds an arbitrary IPv4 destination "3fff::/20" # documentation (RFC 9637) ].map { |r| IPAddr.new(r) }.freeze
- OPEN_TIMEOUT =
2- READ_TIMEOUT =
3- TOTAL_BUDGET =
A ceiling on the WHOLE resolution: DNS plus connect plus read. read_timeout only bounds the gap between reads, so a server trickling bytes forever never trips it, and Ruby's resolver has its own multi-second retry ladder outside both socket timeouts.
5- MAX_BYTES =
64 * 1024
- MAX_REDIRECT_URIS =
20- HOST_FAILURE =
Nothing at that host answered — may block the host's other documents. Distinct from a document-level failure (plain nil), which must not, or one bogus URL would take an entire CIMD-hosting domain down for everyone on it.
:host_failure
Class Method Summary collapse
-
.call(client_id, uri) ⇒ Object
[document, ttl] on success (TTL derived from the document's own cache headers, clamped by config), nil for an unusable document, HOST_FAILURE when nothing at the host answered.
Class Method Details
.call(client_id, uri) ⇒ Object
[document, ttl] on success (TTL derived from the document's own cache headers, clamped by config), nil for an unusable document, HOST_FAILURE when nothing at the host answered.
69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 |
# File 'app/models/hitch/client_id_metadata/fetcher.rb', line 69 def call(client_id, uri) Timeout.timeout(TOTAL_BUDGET) do address = safe_address(uri.host) return HOST_FAILURE if address.nil? fetched = fetch(uri, address) return HOST_FAILURE if fetched == HOST_FAILURE return nil if fetched.nil? body, ttl = fetched document = build_document(client_id, body) document && [ document, ttl ] end rescue Timeout::Error => e log_rejection(client_id, "#{e.class}: #{e.}") HOST_FAILURE rescue JSON::ParserError => e log_rejection(client_id, "#{e.class}: #{e.}") nil end |