# frozen_string_literal: true require 'uri' class WebhookEndpointValidator < ActiveModel::EachValidator def validate_each(record, attribute, value) return if value.blank? unless self.class.safe_webhook_uri?(value) record.errors.add attribute, :invalid end end def self.safe_webhook_uri?(value) uri = value.is_a?(URI) ? value : URI.parse(value) return false if uri.nil? return false unless valid_scheme?(uri.scheme) return false unless valid_host?(uri.host) return false unless valid_port?(uri.port) true rescue Rails.logger.warn { "URI failed webhook safety checks: #{uri}" } false end def self.valid_port?(port) !BAD_PORTS.include?(port) end def self.valid_scheme?(scheme) %w[http https].include?(scheme) end def self.blocked_hosts @blocked_hosts ||= begin ips = [] wildcards = [] hosts = [] Array(Redmine::Configuration['webhook_blocklist']).map(&:to_s).each do |block| # We try to parse the block as an IP address first... ips << IPAddr.new(block) rescue IPAddr::Error # If that failed, we assume it is a (wildcard) hostname if block.start_with?('*.') wildcards << Regexp.escape(block[2..]) else hosts << Regexp.escape(block) end end regex_parts = [] regex_parts << "(?:#{hosts.join('|')})" if hosts.any? regex_parts << "(?:.*\\.)?(?:#{wildcards.join('|')})" if wildcards.any? { ips: ips.freeze, host: regex_parts.any? ? /\A(?:#{regex_parts.join('|')})\z/i : nil }.freeze end end def self.valid_host?(host) return false if host.blank? return false if blocked_hosts[:host]&.match?(host) Resolv.each_address(host) do |ip| ipaddr = IPAddr.new(ip) return false if ipaddr.link_local? || ipaddr.loopback? return false if IPAddr.new('224.0.0.0/24').include?(ipaddr) # multicast return false if blocked_hosts[:ips].any? { |net| net.include?(ipaddr) } end true end # A general port blacklist. Connections to these ports will not be allowed # unless the protocol overrides. # # This list is to be kept in sync with "bad ports" as defined in the # WHATWG Fetch standard at https://fetch.spec.whatwg.org/#port-blocking # # see also: https://github.com/mozilla/gecko-dev/blob/d55e89d48a8053ce45a74b0ec92c0ff6a9dcc43d/netwerk/base/nsIOService.cpp#L109-L199 # BAD_PORTS = Set[ 1, # tcpmux 7, # echo 9, # discard 11, # systat 13, # daytime 15, # netstat 17, # qotd 19, # chargen 20, # ftp-data 21, # ftp 22, # ssh 23, # telnet 25, # smtp 37, # time 42, # name 43, # nicname 53, # domain 69, # tftp 77, # priv-rjs 79, # finger 87, # ttylink 95, # supdup 101, # hostriame 102, # iso-tsap 103, # gppitnp 104, # acr-nema 109, # pop2 110, # pop3 111, # sunrpc 113, # auth 115, # sftp 117, # uucp-path 119, # nntp 123, # ntp 135, # loc-srv / epmap 137, # netbios 139, # netbios 143, # imap2 161, # snmp 179, # bgp 389, # ldap 427, # afp (alternate) 465, # smtp (alternate) 512, # print / exec 513, # login 514, # shell 515, # printer 526, # tempo 530, # courier 531, # chat 532, # netnews 540, # uucp 548, # afp 554, # rtsp 556, # remotefs 563, # nntp+ssl 587, # smtp (outgoing) 601, # syslog-conn 636, # ldap+ssl 989, # ftps-data 990, # ftps 993, # imap+ssl 995, # pop3+ssl 1719, # h323gatestat 1720, # h323hostcall 1723, # pptp 2049, # nfs 3659, # apple-sasl 4045, # lockd 4190, # sieve 5060, # sip 5061, # sips 6000, # x11 6566, # sane-port 6665, # irc (alternate) 6666, # irc (alternate) 6667, # irc (default) 6668, # irc (alternate) 6669, # irc (alternate) 6679, # osaut 6697, # irc+tls 10080 # amanda ].freeze end