summaryrefslogtreecommitdiff
path: root/app/validators/url_validator.rb
blob: 8648c4c75e3d2bd6e25a5237824685cee1b325d3 (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
# 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.
#
# 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

    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
    # 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
    }
  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(:allow_localhost, :allow_local_network, :protocols, :ports).tap do |args|
      if allow_setting_local_requests?
        args[:allow_localhost] = args[:allow_local_network] = true
      end
    end
  end

  def allow_setting_local_requests?
    ApplicationSetting.current&.allow_local_requests_from_hooks_and_services?
  end
end