Module: LcpRuby::UrlSafety

Defined in:
lib/lcp_ruby/url_safety.rb

Overview

Shared URL safety utilities for outbound links and HTTP requests. Used by UrlLink renderer (display) and CallWebhookAction (background jobs).

Defined Under Namespace

Classes: UnsafeUrlError

Constant Summary collapse

%w[http https].freeze
SAFE_HTTP_SCHEMES =
%w[http https].freeze
BLOCKED_NETWORKS =
[
  IPAddr.new("0.0.0.0/8"),
  IPAddr.new("10.0.0.0/8"),
  IPAddr.new("100.64.0.0/10"),
  IPAddr.new("127.0.0.0/8"),
  IPAddr.new("169.254.0.0/16"),
  IPAddr.new("172.16.0.0/12"),
  IPAddr.new("192.168.0.0/16"),
  IPAddr.new("198.18.0.0/15"),
  IPAddr.new("224.0.0.0/4"),
  IPAddr.new("240.0.0.0/4"),
  IPAddr.new("::/128"),
  IPAddr.new("::1/128"),
  IPAddr.new("fc00::/7"),
  IPAddr.new("fe80::/10"),
  IPAddr.new("ff00::/8")
].freeze

Class Method Summary collapse

Class Method Details

.blocked_address?(address) ⇒ Boolean

Returns:

  • (Boolean)


93
94
95
# File 'lib/lcp_ruby/url_safety.rb', line 93

def blocked_address?(address)
  BLOCKED_NETWORKS.any? { |network| network.include?(address) }
end

.ip_literal?(host) ⇒ Boolean

Returns:

  • (Boolean)


81
82
83
84
85
86
# File 'lib/lcp_ruby/url_safety.rb', line 81

def ip_literal?(host)
  IPAddr.new(host)
  true
rescue IPAddr::InvalidAddressError
  false
end

.localhost?(host) ⇒ Boolean

Returns:

  • (Boolean)


88
89
90
91
# File 'lib/lcp_ruby/url_safety.rb', line 88

def localhost?(host)
  normalized = host.to_s.downcase
  normalized == "localhost" || normalized.end_with?(".localhost")
end

.reject_host!(host) ⇒ Object

Raises:



61
62
63
64
65
66
67
# File 'lib/lcp_ruby/url_safety.rb', line 61

def reject_host!(host)
  raise UnsafeUrlError, "URL host is not allowed" if localhost?(host)

  addresses = resolve_addresses(host)
  raise UnsafeUrlError, "URL host could not be resolved" if addresses.empty?
  raise UnsafeUrlError, "URL host resolves to a private or loopback address" if addresses.any? { |address| blocked_address?(address) }
end

.resolve_addresses(host) ⇒ Object



69
70
71
72
73
74
75
76
77
78
79
# File 'lib/lcp_ruby/url_safety.rb', line 69

def resolve_addresses(host)
  return [ IPAddr.new(host) ] if ip_literal?(host)

  Resolv.getaddresses(host).filter_map do |address|
    IPAddr.new(address)
  rescue IPAddr::InvalidAddressError
    nil
  end
rescue Resolv::ResolvError, SocketError
  []
end

.safe_external_link?(value) ⇒ Boolean

Check whether a value is safe to render as an <a href=“…”> link. Returns true only for http/https URLs with a host present.

Returns:

  • (Boolean)


35
36
37
38
39
40
41
42
43
# File 'lib/lcp_ruby/url_safety.rb', line 35

def safe_external_link?(value)
  return false if value.blank?

  href = value.to_s.strip
  uri = URI.parse(href)
  SAFE_LINK_SCHEMES.include?(uri.scheme.to_s.downcase) && uri.host.present?
rescue URI::InvalidURIError
  false
end

.validate_outbound_http_url!(value) ⇒ Object

Validate a URL for outbound HTTP requests (webhooks, etc.). Raises UnsafeUrlError if the URL is not safe. Returns the parsed URI on success.



48
49
50
51
52
53
54
55
56
57
58
59
# File 'lib/lcp_ruby/url_safety.rb', line 48

def validate_outbound_http_url!(value)
  uri = URI.parse(value.to_s)
  scheme = uri.scheme.to_s.downcase

  raise UnsafeUrlError, "URL must use http or https" unless SAFE_HTTP_SCHEMES.include?(scheme)
  raise UnsafeUrlError, "URL host is missing" if uri.host.to_s.empty?

  reject_host!(uri.host)
  uri
rescue URI::InvalidURIError => e
  raise UnsafeUrlError, "Invalid URL: #{e.message}"
end