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

Class Method Details

._resolverObject

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.

Parameters:

  • url (String)

    URL to validate

  • allow_private (Boolean) (defaults to: false)

    when true, bypass the IP-blocklist check

Returns:

  • (Boolean)

    true if the 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.message}")
  false
end