diff options
author | Thong Kuah <tkuah@gitlab.com> | 2019-04-11 06:29:07 +0000 |
---|---|---|
committer | James Lopez <james@gitlab.com> | 2019-04-11 06:29:07 +0000 |
commit | d119d3d1b25aac661e6251addf87b280bd37f0c5 (patch) | |
tree | aeaf0d9503326ec7f51968e8d1de48d83ce90503 /app/validators | |
parent | 79bf4bdaad438dc0f82771b102f3c07225a428da (diff) | |
download | gitlab-ce-d119d3d1b25aac661e6251addf87b280bd37f0c5.tar.gz |
Align UrlValidator to validate_url gem implementation.
Renamed UrlValidator to AddressableUrlValidator to avoid 'url:' naming collision with ActiveModel::Validations::UrlValidator in 'validates' statement.
Make use of the options attribute of the parent class ActiveModel::EachValidator.
Add more options: allow_nil, allow_blank, message.
Renamed 'protocols' option to 'schemes' to match the option naming from UrlValidator.
Diffstat (limited to 'app/validators')
-rw-r--r-- | app/validators/addressable_url_validator.rb | 112 | ||||
-rw-r--r-- | app/validators/public_url_validator.rb | 19 | ||||
-rw-r--r-- | app/validators/url_validator.rb | 104 |
3 files changed, 123 insertions, 112 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 diff --git a/app/validators/public_url_validator.rb b/app/validators/public_url_validator.rb index 3ff880deedd..91847c5d866 100644 --- a/app/validators/public_url_validator.rb +++ b/app/validators/public_url_validator.rb @@ -2,7 +2,7 @@ # PublicUrlValidator # -# Custom validator for URLs. This validator works like UrlValidator but +# Custom validator for URLs. This validator works like AddressableUrlValidator but # it blocks by default urls pointing to localhost or the local network. # # This validator accepts the same params UrlValidator does. @@ -12,17 +12,20 @@ # class User < ActiveRecord::Base # validates :personal_url, public_url: true # -# validates :ftp_url, public_url: { protocols: %w(ftp) } +# validates :ftp_url, public_url: { schemes: %w(ftp) } # # validates :git_url, public_url: { allow_localhost: true, allow_local_network: true} # end # -class PublicUrlValidator < UrlValidator - private +class PublicUrlValidator < AddressableUrlValidator + DEFAULT_OPTIONS = { + allow_localhost: false, + allow_local_network: false + }.freeze - def default_options - # By default block all urls pointing to localhost or the local network - super.merge(allow_localhost: false, - allow_local_network: false) + def initialize(options) + options.reverse_merge!(DEFAULT_OPTIONS) + + super(options) end end diff --git a/app/validators/url_validator.rb b/app/validators/url_validator.rb deleted file mode 100644 index 3fd015c3cf5..00000000000 --- a/app/validators/url_validator.rb +++ /dev/null @@ -1,104 +0,0 @@ -# frozen_string_literal: true - -# UrlValidator -# -# Custom validator for URLs. -# -# By default, only URLs for the HTTP(S) protocols will be considered valid. -# Provide a `:protocols` option to configure accepted protocols. -# -# Example: -# -# class User < ActiveRecord::Base -# validates :personal_url, url: true -# -# validates :ftp_url, url: { protocols: %w(ftp) } -# -# 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 -# - enforce_sanitization: Validate that there are no html/css/js tags. 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) - @record = record - - unless value.present? - record.errors.add(attribute, 'must be a valid URL') - 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 default_options - # 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, - ascii_only: false, - enforce_user: false, - enforce_sanitization: false - } - end - - def current_options - options = self.options.map do |option, value| - [option, value.is_a?(Proc) ? value.call(record) : value] - end.to_h - - 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 - - def 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 |