url_blocker.rb 3.49 KB
Newer Older
1 2 3 4
require 'resolv'

module Gitlab
  class UrlBlocker
5
    BlockedUrlError = Class.new(StandardError)
6

7
    class << self
8
      def validate!(url, allow_localhost: false, allow_local_network: true, enforce_user: false, ports: [], protocols: [])
9
        return true if url.nil?
10 11 12

        begin
          uri = Addressable::URI.parse(url)
13 14 15
        rescue Addressable::URI::InvalidURIError
          raise BlockedUrlError, "URI is invalid"
        end
16

17 18
        # Allow imports from the GitLab instance itself but only from the configured ports
        return true if internal?(uri)
19

20
        port = uri.port || uri.default_port
21 22
        validate_protocol!(uri.scheme, protocols)
        validate_port!(port, ports) if ports.any?
23
        validate_user!(uri.user) if enforce_user
24
        validate_hostname!(uri.hostname)
25

26 27
        begin
          addrs_info = Addrinfo.getaddrinfo(uri.hostname, port, nil, :STREAM)
28
        rescue SocketError
29
          return true
30 31
        end

32
        validate_localhost!(addrs_info) unless allow_localhost
33
        validate_local_network!(addrs_info) unless allow_local_network
34
        validate_link_local!(addrs_info) unless allow_local_network
35

36 37 38 39 40 41
        true
      end

      def blocked_url?(*args)
        validate!(*args)

42
        false
43 44
      rescue BlockedUrlError
        true
45 46 47 48
      end

      private

49
      def validate_port!(port, ports)
50 51 52
        return if port.blank?
        # Only ports under 1024 are restricted
        return if port >= 1024
53
        return if ports.include?(port)
54

55 56 57 58 59 60 61
        raise BlockedUrlError, "Only allowed ports are #{ports.join(', ')}, and any over 1024"
      end

      def validate_protocol!(protocol, protocols)
        if protocol.blank? || (protocols.any? && !protocols.include?(protocol))
          raise BlockedUrlError, "Only allowed protocols are #{protocols.join(', ')}"
        end
62 63 64 65 66
      end

      def validate_user!(value)
        return if value.blank?
        return if value =~ /\A\p{Alnum}/
67

68
        raise BlockedUrlError, "Username needs to start with an alphanumeric character"
69 70
      end

71 72 73
      def validate_hostname!(value)
        return if value.blank?
        return if value =~ /\A\p{Alnum}/
74

75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90
        raise BlockedUrlError, "Hostname needs to start with an alphanumeric character"
      end

      def validate_localhost!(addrs_info)
        local_ips = ["127.0.0.1", "::1", "0.0.0.0"]
        local_ips.concat(Socket.ip_address_list.map(&:ip_address))

        return if (local_ips & addrs_info.map(&:ip_address)).empty?

        raise BlockedUrlError, "Requests to localhost are not allowed"
      end

      def validate_local_network!(addrs_info)
        return unless addrs_info.any? { |addr| addr.ipv4_private? || addr.ipv6_sitelocal? }

        raise BlockedUrlError, "Requests to the local network are not allowed"
91 92
      end

93 94 95 96 97 98 99
      def validate_link_local!(addrs_info)
        netmask = IPAddr.new('169.254.0.0/16')
        return unless addrs_info.any? { |addr| addr.ipv6_linklocal? || netmask.include?(addr.ip_address) }

        raise BlockedUrlError, "Requests to the link local network are not allowed"
      end

100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119
      def internal?(uri)
        internal_web?(uri) || internal_shell?(uri)
      end

      def internal_web?(uri)
        uri.hostname == config.gitlab.host &&
          (uri.port.blank? || uri.port == config.gitlab.port)
      end

      def internal_shell?(uri)
        uri.hostname == config.gitlab_shell.ssh_host &&
          (uri.port.blank? || uri.port == config.gitlab_shell.ssh_port)
      end

      def config
        Gitlab.config
      end
    end
  end
end