diff options
Diffstat (limited to 'app/validators/url_validator.rb')
-rw-r--r-- | app/validators/url_validator.rb | 62 |
1 files changed, 54 insertions, 8 deletions
diff --git a/app/validators/url_validator.rb b/app/validators/url_validator.rb index a77beb2683d..6854fec582e 100644 --- a/app/validators/url_validator.rb +++ b/app/validators/url_validator.rb @@ -15,25 +15,71 @@ # validates :git_url, url: { protocols: %w(http https ssh git) } # end # +# This validator can also block urls pointing to localhost or the local network to +# protect against Server-side Request Forgery (SSRF), or check for the right port. +# +# The available options are: +# - protocols: Allowed protocols. Default: http and https +# - allow_localhost: Allow urls pointing to localhost. Default: true +# - allow_local_network: Allow urls pointing to private network addresses. Default: true +# - ports: Allowed ports. Default: all. +# - enforce_user: Validate user format. Default: false +# +# Example: +# class User < ActiveRecord::Base +# validates :personal_url, url: { allow_localhost: false, allow_local_network: false} +# +# validates :web_url, url: { ports: [80, 443] } +# end class UrlValidator < ActiveModel::EachValidator + DEFAULT_PROTOCOLS = %w(http https).freeze + + attr_reader :record + def validate_each(record, attribute, value) - unless valid_url?(value) - record.errors.add(attribute, "must be a valid URL") + @record = record + + if value.present? + value.strip! + else + record.errors.add(attribute, 'must be a valid URL') end + + Gitlab::UrlBlocker.validate!(value, blocker_args) + rescue Gitlab::UrlBlocker::BlockedUrlError => e + record.errors.add(attribute, "is blocked: #{e.message}") end private def default_options - @default_options ||= { protocols: %w(http https) } + # By default the validator doesn't block any url based on the ip address + { + protocols: DEFAULT_PROTOCOLS, + ports: [], + allow_localhost: true, + allow_local_network: true, + enforce_user: false + } end - def valid_url?(value) - return false if value.nil? + def current_options + options = self.options.map do |option, value| + [option, value.is_a?(Proc) ? value.call(record) : value] + end.to_h - options = default_options.merge(self.options) + default_options.merge(options) + end + + def blocker_args + current_options.slice(*default_options.keys).tap do |args| + if allow_setting_local_requests? + args[:allow_localhost] = args[:allow_local_network] = true + end + end + end - value.strip! - value =~ /\A#{URI.regexp(options[:protocols])}\z/ + def allow_setting_local_requests? + ApplicationSetting.current&.allow_local_requests_from_hooks_and_services? end end |