Module: SignalWire::Utils::UrlValidator
- Defined in:
- lib/signalwire/utils/url_validator.rb
Overview
SSRF-prevention guard for user-supplied URLs.
Mirrors Python’s signalwire.utils.url_validator.validate_url: rejects non-http(s) schemes, missing hostnames, and any URL whose hostname resolves to a private / loopback / link-local / cloud- metadata IP. When allow_private is true, OR the SWML_ALLOW_PRIVATE_URLS env var is set to “1”, “true” or “yes” (case-insensitive), the IP-blocklist check is skipped.
The method UrlValidator.validate_url projects onto the Python free function signalwire.utils.url_validator.validate_url via scripts/enumerate_signatures.py.
Constant Summary collapse
- BLOCKED_NETWORKS =
Cross-port SSRF block list. Order matches the Python reference.
%w[ 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 127.0.0.0/8 169.254.0.0/16 0.0.0.0/8 ::1/128 fc00::/7 fe80::/10 ].freeze
- LOG =
SignalWire::Logging.logger('signalwire.url_validator')
Class Method Summary collapse
-
._resolver ⇒ Object
Pluggable resolver hook.
- ._resolver=(value) ⇒ Object
-
.validate_url(url, allow_private = false) ⇒ Boolean
Validate that a URL is safe to fetch.
Class Method Details
._resolver ⇒ Object
Pluggable resolver hook. Tests inject a lambda to keep the suite hermetic; production calls Resolv.getaddresses. Underscore prefix keeps it out of the public surface inventory — the Python reference only exposes “validate_url“ at this module level.
48 49 50 |
# File 'lib/signalwire/utils/url_validator.rb', line 48 def self._resolver @_resolver end |
._resolver=(value) ⇒ Object
52 53 54 |
# File 'lib/signalwire/utils/url_validator.rb', line 52 def self._resolver=(value) @_resolver = value end |
.validate_url(url, allow_private = false) ⇒ Boolean
Validate that a URL is safe to fetch.
61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 |
# File 'lib/signalwire/utils/url_validator.rb', line 61 def self.validate_url(url, allow_private = false) parsed = URI.parse(url) scheme = (parsed.scheme || '').downcase unless %w[http https].include?(scheme) LOG.warn("URL rejected: invalid scheme #{parsed.scheme}") return false end hostname = parsed.host if hostname.nil? || hostname.empty? LOG.warn('URL rejected: no hostname') return false end # URI keeps brackets in host for IPv6 literals; strip them. if hostname.start_with?('[') && hostname.end_with?(']') hostname = hostname[1..-2] end if allow_private || _env_allows_private? return true end ips = _resolve(hostname) if ips.nil? || ips.empty? LOG.warn("URL rejected: could not resolve hostname #{hostname}") return false end ips.each do |ip_str| ip = begin IPAddr.new(ip_str) rescue IPAddr::InvalidAddressError next end BLOCKED_NETWORKS.each do |cidr| net = IPAddr.new(cidr) if net.include?(ip) LOG.warn("URL rejected: #{hostname} resolves to blocked IP #{ip_str} (in #{cidr})") return false end end end true rescue URI::InvalidURIError, IPAddr::InvalidAddressError => e LOG.warn("URL validation error: #{e.}") false end |