Class: RubyLLM::Toolbox::Safety::UrlGuard

Inherits:
Object
  • Object
show all
Defined in:
lib/ruby_llm/toolbox/safety/url_guard.rb

Overview

SSRF defense for tools that fetch arbitrary URLs. It:

- allows only http/https,
- rejects embedded credentials,
- enforces optional domain allow/deny lists,
- resolves the host and blocks the request if ANY resolved address is
  private, loopback, link-local (incl. the cloud metadata IP), CGNAT,
  unique-local IPv6, or otherwise reserved.

resolve! also returns a vetted IP so the caller can pin the socket to it (Net::HTTP#ipaddr=), closing the DNS-rebinding window: the address that was vetted is exactly the one connected to, while TLS/SNI/cert checks still use the hostname. Re-run resolve! on every redirect hop (the fetch helpers do).

Defined Under Namespace

Classes: Blocked, Resolution

Constant Summary collapse

ALLOWED_SCHEMES =
%w[http https].freeze
BLOCKED_RANGES =
%w[
  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.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
  ::1/128 ::/128 fc00::/7 fe80::/10
].map { |cidr| IPAddr.new(cidr) }.freeze

Instance Method Summary collapse

Constructor Details

#initialize(allowlist: [], denylist: []) ⇒ UrlGuard

Returns a new instance of UrlGuard.



40
41
42
43
# File 'lib/ruby_llm/toolbox/safety/url_guard.rb', line 40

def initialize(allowlist: [], denylist: [])
  @allowlist = Array(allowlist).map { |d| d.to_s.downcase }
  @denylist  = Array(denylist).map { |d| d.to_s.downcase }
end

Instance Method Details

#check!(url) ⇒ Object

Returns a parsed URI if the URL is safe to fetch; raises Blocked otherwise.



47
48
49
# File 'lib/ruby_llm/toolbox/safety/url_guard.rb', line 47

def check!(url)
  resolve!(url).uri
end

#resolve!(url) ⇒ Object

Like check!, but also returns a vetted IP address to pin the connection to (see Resolution). Raises Blocked otherwise.

Raises:



53
54
55
56
57
58
59
60
61
62
63
# File 'lib/ruby_llm/toolbox/safety/url_guard.rb', line 53

def resolve!(url)
  uri = parse(url)
  raise Blocked, "only http/https URLs are allowed" unless ALLOWED_SCHEMES.include?(uri.scheme)
  raise Blocked, "URL must include a host" if uri.host.nil? || uri.host.empty?
  raise Blocked, "URLs with embedded credentials are not allowed" if uri.userinfo

  host = uri.host.downcase
  enforce_domain_lists(host)
  address = vetted_address(host)
  Resolution.new(uri: uri, address: address)
end