Welcome to mirror list, hosted at ThFree Co, Russian Federation.

addressable_url_validator.rb « validators « app - gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: 8fa562a04dd27b81f66994e47d029605b131ba03 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
# 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, used when the URL is blank. (default is: "must be a valid URL").
# * <tt>blocked_message</tt> - A custom error message, used when the URL is blocked. Default: +'is blocked: %{exception_message}'+.
# * <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>deny_all_requests_except_allowed</tt> - Deny all requests. Default: Respects the instance app setting.
#                                               Note: Regardless of whether enforced during validation, an HTTP request that uses the URI may still be blocked.
# * <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

  # By default, we avoid checking the dns rebinding protection
  # when saving/updating a record. Sometimes, the url
  # is not resolvable at that point, and some automated
  # tasks that uses that url won't work.
  # See https://gitlab.com/gitlab-org/gitlab-foss/issues/66723
  BLOCKER_VALIDATE_OPTIONS = {
    schemes: %w[http https],
    ports: [],
    allow_localhost: true,
    allow_local_network: true,
    ascii_only: false,
    deny_all_requests_except_allowed: false,
    enforce_user: false,
    enforce_sanitization: false,
    dns_rebind_protection: false
  }.freeze

  DEFAULT_OPTIONS = BLOCKER_VALIDATE_OPTIONS.merge({
    message: 'must be a valid URL',
    blocked_message: 'is blocked: %{exception_message}'
  }).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::HTTP_V2::UrlBlocker.validate!(value, **blocker_args)
  rescue Gitlab::HTTP_V2::UrlBlocker::BlockedUrlError => e
    record.errors.add(attribute, options.fetch(:blocked_message) % { exception_message: 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.transform_values do |value|
      value.is_a?(Proc) ? value.call(record) : value
    end
  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

      if deny_all_requests_except_allowed?
        args[:deny_all_requests_except_allowed] = 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/issues/9833
    ApplicationSetting.current&.allow_local_requests_from_web_hooks_and_services?
  end

  def deny_all_requests_except_allowed?
    ApplicationSetting.current&.deny_all_requests_except_allowed?
  end
end