diff options
Diffstat (limited to 'app/validators/addressable_url_validator.rb')
-rw-r--r-- | app/validators/addressable_url_validator.rb | 112 |
1 files changed, 112 insertions, 0 deletions
diff --git a/app/validators/addressable_url_validator.rb b/app/validators/addressable_url_validator.rb new file mode 100644 index 00000000000..273e15ef925 --- /dev/null +++ b/app/validators/addressable_url_validator.rb @@ -0,0 +1,112 @@ +# frozen_string_literal: true + +# AddressableUrlValidator +# +# Custom validator for URLs. This is a stricter version of UrlValidator - it also checks +# for using the right protocol, but it actually parses the URL checking for any syntax errors. +# The regex is also different from `URI` as we use `Addressable::URI` here. +# +# By default, only URLs for the HTTP(S) schemes will be considered valid. +# Provide a `:schemes` option to configure accepted schemes. +# +# Example: +# +# class User < ActiveRecord::Base +# validates :personal_url, addressable_url: true +# +# validates :ftp_url, addressable_url: { schemes: %w(ftp) } +# +# validates :git_url, addressable_url: { schemes: %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. +# +# Configuration options: +# * <tt>message</tt> - A custom error message (default is: "must be a valid URL"). +# * <tt>schemes</tt> - Array of URI schemes. Default: +['http', 'https']+ +# * <tt>allow_localhost</tt> - Allow urls pointing to +localhost+. Default: +true+ +# * <tt>allow_local_network</tt> - Allow urls pointing to private network addresses. Default: +true+ +# * <tt>allow_blank</tt> - Allow urls to be +blank+. Default: +false+ +# * <tt>allow_nil</tt> - Allow urls to be +nil+. Default: +false+ +# * <tt>ports</tt> - Allowed ports. Default: +all+. +# * <tt>enforce_user</tt> - Validate user format. Default: +false+ +# * <tt>enforce_sanitization</tt> - Validate that there are no html/css/js tags. Default: +false+ +# +# Example: +# class User < ActiveRecord::Base +# validates :personal_url, addressable_url: { allow_localhost: false, allow_local_network: false} +# +# validates :web_url, addressable_url: { ports: [80, 443] } +# end +class AddressableUrlValidator < ActiveModel::EachValidator + attr_reader :record + + BLOCKER_VALIDATE_OPTIONS = { + schemes: %w(http https), + ports: [], + allow_localhost: true, + allow_local_network: true, + ascii_only: false, + enforce_user: false, + enforce_sanitization: false + }.freeze + + DEFAULT_OPTIONS = BLOCKER_VALIDATE_OPTIONS.merge({ + message: 'must be a valid URL' + }).freeze + + def initialize(options) + options.reverse_merge!(DEFAULT_OPTIONS) + + super(options) + end + + def validate_each(record, attribute, value) + @record = record + + unless value.present? + record.errors.add(attribute, options.fetch(:message)) + return + end + + value = strip_value!(record, attribute, value) + + Gitlab::UrlBlocker.validate!(value, blocker_args) + rescue Gitlab::UrlBlocker::BlockedUrlError => e + record.errors.add(attribute, "is blocked: #{e.message}") + end + + private + + def strip_value!(record, attribute, value) + new_value = value.strip + return value if new_value == value + + record.public_send("#{attribute}=", new_value) # rubocop:disable GitlabSecurity/PublicSend + end + + def current_options + options.map do |option, value| + [option, value.is_a?(Proc) ? value.call(record) : value] + end.to_h + end + + def blocker_args + current_options.slice(*BLOCKER_VALIDATE_OPTIONS.keys).tap do |args| + if self.class.allow_setting_local_requests? + args[:allow_localhost] = args[:allow_local_network] = true + end + end + end + + def self.allow_setting_local_requests? + # We cannot use Gitlab::CurrentSettings as ApplicationSetting itself + # uses UrlValidator to validate urls. This ends up in a cycle + # when Gitlab::CurrentSettings creates an ApplicationSetting which then + # calls this validator. + # + # See https://gitlab.com/gitlab-org/gitlab-ee/issues/9833 + ApplicationSetting.current&.allow_local_requests_from_hooks_and_services? + end +end |