[ADD] outbound requests whitelist
Signed-off-by: Istvan szalai <istvan.szalai@savoirfairelinux.com>
This commit is contained in:
		
							parent
							
								
									6a5d2df3ee
								
							
						
					
					
						commit
						e5bdcfbc9b
					
				| 
						 | 
				
			
			@ -177,6 +177,7 @@ module ApplicationSettingsHelper
 | 
			
		|||
      :domain_blacklist_enabled,
 | 
			
		||||
      :domain_blacklist_raw,
 | 
			
		||||
      :domain_whitelist_raw,
 | 
			
		||||
      :outbound_local_requests_whitelist_raw,
 | 
			
		||||
      :dsa_key_restriction,
 | 
			
		||||
      :ecdsa_key_restriction,
 | 
			
		||||
      :ed25519_key_restriction,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -41,6 +41,11 @@ class ApplicationSetting < ApplicationRecord
 | 
			
		|||
 | 
			
		||||
  validates :uuid, presence: true
 | 
			
		||||
 | 
			
		||||
  validates :outbound_local_requests_whitelist,
 | 
			
		||||
            length: { maximum: 1_000, message: N_('is too long (maximum is 1000 entries)') }
 | 
			
		||||
 | 
			
		||||
  validates :outbound_local_requests_whitelist, qualified_domain_array: true, allow_blank: true
 | 
			
		||||
 | 
			
		||||
  validates :session_expire_delay,
 | 
			
		||||
            presence: true,
 | 
			
		||||
            numericality: { only_integer: true, greater_than_or_equal_to: 0 }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,6 +2,7 @@
 | 
			
		|||
 | 
			
		||||
module ApplicationSettingImplementation
 | 
			
		||||
  extend ActiveSupport::Concern
 | 
			
		||||
  include Gitlab::Utils::StrongMemoize
 | 
			
		||||
 | 
			
		||||
  DOMAIN_LIST_SEPARATOR = %r{\s*[,;]\s*     # comma or semicolon, optionally surrounded by whitespace
 | 
			
		||||
                            |               # or
 | 
			
		||||
| 
						 | 
				
			
			@ -96,7 +97,8 @@ module ApplicationSettingImplementation
 | 
			
		|||
        diff_max_patch_bytes: Gitlab::Git::Diff::DEFAULT_MAX_PATCH_BYTES,
 | 
			
		||||
        commit_email_hostname: default_commit_email_hostname,
 | 
			
		||||
        protected_ci_variables: false,
 | 
			
		||||
        local_markdown_version: 0
 | 
			
		||||
        local_markdown_version: 0,
 | 
			
		||||
        outbound_local_requests_whitelist: []
 | 
			
		||||
      }
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -131,31 +133,52 @@ module ApplicationSettingImplementation
 | 
			
		|||
  end
 | 
			
		||||
 | 
			
		||||
  def domain_whitelist_raw
 | 
			
		||||
    self.domain_whitelist&.join("\n")
 | 
			
		||||
    array_to_string(self.domain_whitelist)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def domain_blacklist_raw
 | 
			
		||||
    self.domain_blacklist&.join("\n")
 | 
			
		||||
    array_to_string(self.domain_blacklist)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def domain_whitelist_raw=(values)
 | 
			
		||||
    self.domain_whitelist = []
 | 
			
		||||
    self.domain_whitelist = values.split(DOMAIN_LIST_SEPARATOR)
 | 
			
		||||
    self.domain_whitelist.reject! { |d| d.empty? }
 | 
			
		||||
    self.domain_whitelist
 | 
			
		||||
    self.domain_whitelist = domain_strings_to_array(values)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def domain_blacklist_raw=(values)
 | 
			
		||||
    self.domain_blacklist = []
 | 
			
		||||
    self.domain_blacklist = values.split(DOMAIN_LIST_SEPARATOR)
 | 
			
		||||
    self.domain_blacklist.reject! { |d| d.empty? }
 | 
			
		||||
    self.domain_blacklist
 | 
			
		||||
    self.domain_blacklist = domain_strings_to_array(values)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def domain_blacklist_file=(file)
 | 
			
		||||
    self.domain_blacklist_raw = file.read
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def outbound_local_requests_whitelist_raw
 | 
			
		||||
    array_to_string(self.outbound_local_requests_whitelist)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def outbound_local_requests_whitelist_raw=(values)
 | 
			
		||||
    self.outbound_local_requests_whitelist = domain_strings_to_array(values)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def outbound_local_requests_whitelist_arrays
 | 
			
		||||
    strong_memoize(:outbound_local_requests_whitelist_arrays) do
 | 
			
		||||
      ip_whitelist = []
 | 
			
		||||
      domain_whitelist = []
 | 
			
		||||
 | 
			
		||||
      self.outbound_local_requests_whitelist.each do |str|
 | 
			
		||||
        ip_obj = Gitlab::Utils.string_to_ip_object(str)
 | 
			
		||||
 | 
			
		||||
        if ip_obj
 | 
			
		||||
          ip_whitelist << ip_obj
 | 
			
		||||
        else
 | 
			
		||||
          domain_whitelist << str
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      [ip_whitelist, domain_whitelist]
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def repository_storages
 | 
			
		||||
    Array(read_attribute(:repository_storages))
 | 
			
		||||
  end
 | 
			
		||||
| 
						 | 
				
			
			@ -255,6 +278,17 @@ module ApplicationSettingImplementation
 | 
			
		|||
 | 
			
		||||
  private
 | 
			
		||||
 | 
			
		||||
  def array_to_string(arr)
 | 
			
		||||
    arr&.join("\n")
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def domain_strings_to_array(values)
 | 
			
		||||
    values
 | 
			
		||||
      .split(DOMAIN_LIST_SEPARATOR)
 | 
			
		||||
      .reject(&:empty?)
 | 
			
		||||
      .uniq
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def ensure_uuid!
 | 
			
		||||
    return if uuid?
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,6 +8,13 @@
 | 
			
		|||
        = f.label :allow_local_requests_from_hooks_and_services, class: 'form-check-label' do
 | 
			
		||||
          Allow requests to the local network from hooks and services
 | 
			
		||||
 | 
			
		||||
    .form-group
 | 
			
		||||
      = f.label :outbound_local_requests_whitelist_raw, class: 'label-bold' do
 | 
			
		||||
        = _('Whitelist to allow requests to the local network from hooks and services')
 | 
			
		||||
      = f.text_area :outbound_local_requests_whitelist_raw, placeholder: "example.com, 192.168.1.1", class: 'form-control', rows: 8
 | 
			
		||||
      %span.form-text.text-muted
 | 
			
		||||
        = _('Requests to these domain(s)/address(es) on the local network will be allowed when local requests from hooks and services are disabled. IP ranges such as 1:0:0:0:0:0:0:0/124 or 127.0.0.0/28 are supported. Domain wildcards are not supported currently. Use comma, semicolon, or newline to separate multiple entries. The whitelist can hold a maximum of 4000 entries. Domains should use IDNA encoding. Ex: domain.com, 192.168.1.1, 127.0.0.0/28.')
 | 
			
		||||
 | 
			
		||||
    .form-group
 | 
			
		||||
      .form-check
 | 
			
		||||
        = f.check_box :dns_rebinding_protection_enabled, class: 'form-check-input'
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,5 @@
 | 
			
		|||
---
 | 
			
		||||
title: Add Outbound requests whitelist for local networks
 | 
			
		||||
merge_request: 30350
 | 
			
		||||
author: Istvan Szalai
 | 
			
		||||
type: added
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,9 @@
 | 
			
		|||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
class AddOutboundRequestsWhitelistToApplicationSettings < ActiveRecord::Migration[5.1]
 | 
			
		||||
  DOWNTIME = false
 | 
			
		||||
 | 
			
		||||
  def change
 | 
			
		||||
    add_column :application_settings, :outbound_local_requests_whitelist, :string, array: true, limit: 255
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -228,6 +228,7 @@ ActiveRecord::Schema.define(version: 2019_07_15_114644) do
 | 
			
		|||
    t.boolean "lock_memberships_to_ldap", default: false, null: false
 | 
			
		||||
    t.boolean "time_tracking_limit_to_hours", default: false, null: false
 | 
			
		||||
    t.string "grafana_url", default: "/-/grafana", null: false
 | 
			
		||||
    t.string "outbound_local_requests_whitelist", limit: 255, array: true
 | 
			
		||||
    t.index ["custom_project_templates_group_id"], name: "index_application_settings_on_custom_project_templates_group_id"
 | 
			
		||||
    t.index ["file_template_project_id"], name: "index_application_settings_on_file_template_project_id"
 | 
			
		||||
    t.index ["usage_stats_set_by_user_id"], name: "index_application_settings_on_usage_stats_set_by_user_id"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -39,6 +39,7 @@ Example response:
 | 
			
		|||
   "session_expire_delay" : 10080,
 | 
			
		||||
   "home_page_url" : null,
 | 
			
		||||
   "default_snippet_visibility" : "private",
 | 
			
		||||
   "outbound_local_requests_whitelist": [],
 | 
			
		||||
   "domain_whitelist" : [],
 | 
			
		||||
   "domain_blacklist_enabled" : false,
 | 
			
		||||
   "domain_blacklist" : [],
 | 
			
		||||
| 
						 | 
				
			
			@ -113,6 +114,7 @@ Example response:
 | 
			
		|||
  "default_project_visibility": "internal",
 | 
			
		||||
  "default_snippet_visibility": "private",
 | 
			
		||||
  "default_group_visibility": "private",
 | 
			
		||||
  "outbound_local_requests_whitelist": [],
 | 
			
		||||
  "domain_whitelist": [],
 | 
			
		||||
  "domain_blacklist_enabled" : false,
 | 
			
		||||
  "domain_blacklist" : [],
 | 
			
		||||
| 
						 | 
				
			
			@ -193,6 +195,7 @@ are listed in the descriptions of the relevant settings.
 | 
			
		|||
| `domain_blacklist`                       | array of strings | required by: `domain_blacklist_enabled` | Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: `domain.com`, `*.domain.com`. |
 | 
			
		||||
| `domain_blacklist_enabled`               | boolean          | no                                   | (**If enabled, requires:** `domain_blacklist`) Allows blocking sign-ups from emails from specific domains. |
 | 
			
		||||
| `domain_whitelist`                       | array of strings | no                                   | Force people to use only corporate emails for sign-up. Default is `null`, meaning there is no restriction. |
 | 
			
		||||
| `outbound_local_requests_whitelist`      | array of strings | no                                   | Define a list of trusted domains or ip addresses to which local requests are allowed when local requests for hooks and services are disabled.
 | 
			
		||||
| `dsa_key_restriction`                    | integer          | no                                   | The minimum allowed bit length of an uploaded DSA key. Default is `0` (no restriction). `-1` disables DSA keys. |
 | 
			
		||||
| `ecdsa_key_restriction`                  | integer          | no                                   | The minimum allowed curve size (in bits) of an uploaded ECDSA key. Default is `0` (no restriction). `-1` disables ECDSA keys. |
 | 
			
		||||
| `ed25519_key_restriction`                | integer          | no                                   | The minimum allowed curve size (in bits) of an uploaded ED25519 key. Default is `0` (no restriction). `-1` disables ED25519 keys. |
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -45,18 +45,21 @@ module Gitlab
 | 
			
		|||
          ascii_only: ascii_only
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        normalized_hostname = uri.normalized_host
 | 
			
		||||
        hostname = uri.hostname
 | 
			
		||||
        port = get_port(uri)
 | 
			
		||||
 | 
			
		||||
        address_info = get_address_info(hostname, port)
 | 
			
		||||
        return [uri, nil] unless address_info
 | 
			
		||||
 | 
			
		||||
        protected_uri_with_hostname = enforce_uri_hostname(address_info, uri, hostname, dns_rebind_protection)
 | 
			
		||||
        ip_address = ip_address(address_info)
 | 
			
		||||
        protected_uri_with_hostname = enforce_uri_hostname(ip_address, uri, hostname, dns_rebind_protection)
 | 
			
		||||
 | 
			
		||||
        # Allow url from the GitLab instance itself but only for the configured hostname and ports
 | 
			
		||||
        return protected_uri_with_hostname if internal?(uri)
 | 
			
		||||
 | 
			
		||||
        validate_local_request(
 | 
			
		||||
          normalized_hostname: normalized_hostname,
 | 
			
		||||
          address_info: address_info,
 | 
			
		||||
          allow_localhost: allow_localhost,
 | 
			
		||||
          allow_local_network: allow_local_network
 | 
			
		||||
| 
						 | 
				
			
			@ -83,10 +86,7 @@ module Gitlab
 | 
			
		|||
      #
 | 
			
		||||
      # The original hostname is used to validate the SSL, given in that scenario
 | 
			
		||||
      # we'll be making the request to the IP address, instead of using the hostname.
 | 
			
		||||
      def enforce_uri_hostname(addrs_info, uri, hostname, dns_rebind_protection)
 | 
			
		||||
        address = addrs_info.first
 | 
			
		||||
        ip_address = address&.ip_address
 | 
			
		||||
 | 
			
		||||
      def enforce_uri_hostname(ip_address, uri, hostname, dns_rebind_protection)
 | 
			
		||||
        return [uri, nil] unless dns_rebind_protection && ip_address && ip_address != hostname
 | 
			
		||||
 | 
			
		||||
        uri = uri.dup
 | 
			
		||||
| 
						 | 
				
			
			@ -94,6 +94,10 @@ module Gitlab
 | 
			
		|||
        [uri, hostname]
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      def ip_address(address_info)
 | 
			
		||||
        address_info.first&.ip_address
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      def validate_uri(uri:, schemes:, ports:, enforce_sanitization:, enforce_user:, ascii_only:)
 | 
			
		||||
        validate_html_tags(uri) if enforce_sanitization
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -113,9 +117,19 @@ module Gitlab
 | 
			
		|||
      rescue SocketError
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      def validate_local_request(address_info:, allow_localhost:, allow_local_network:)
 | 
			
		||||
      def validate_local_request(
 | 
			
		||||
        normalized_hostname:,
 | 
			
		||||
        address_info:,
 | 
			
		||||
        allow_localhost:,
 | 
			
		||||
        allow_local_network:)
 | 
			
		||||
        return if allow_local_network && allow_localhost
 | 
			
		||||
 | 
			
		||||
        ip_whitelist, domain_whitelist =
 | 
			
		||||
          Gitlab::CurrentSettings.outbound_local_requests_whitelist_arrays
 | 
			
		||||
 | 
			
		||||
        return if local_domain_whitelisted?(domain_whitelist, normalized_hostname) ||
 | 
			
		||||
          local_ip_whitelisted?(ip_whitelist, ip_address(address_info))
 | 
			
		||||
 | 
			
		||||
        unless allow_localhost
 | 
			
		||||
          validate_localhost(address_info)
 | 
			
		||||
          validate_loopback(address_info)
 | 
			
		||||
| 
						 | 
				
			
			@ -231,6 +245,16 @@ module Gitlab
 | 
			
		|||
          (uri.port.blank? || uri.port == config.gitlab_shell.ssh_port)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      def local_ip_whitelisted?(ip_whitelist, ip_string)
 | 
			
		||||
        ip_obj = Gitlab::Utils.string_to_ip_object(ip_string)
 | 
			
		||||
 | 
			
		||||
        ip_whitelist.any? { |ip| ip.include?(ip_obj) }
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      def local_domain_whitelisted?(domain_whitelist, domain_string)
 | 
			
		||||
        domain_whitelist.include?(domain_string)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      def config
 | 
			
		||||
        Gitlab.config
 | 
			
		||||
      end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -131,5 +131,12 @@ module Gitlab
 | 
			
		|||
        data
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def string_to_ip_object(str)
 | 
			
		||||
      return unless str
 | 
			
		||||
 | 
			
		||||
      IPAddr.new(str)
 | 
			
		||||
    rescue IPAddr::InvalidAddressError
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8998,6 +8998,9 @@ msgstr ""
 | 
			
		|||
msgid "Requests Profiles"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "Requests to these domain(s)/address(es) on the local network will be allowed when local requests from hooks and services are disabled. IP ranges such as 1:0:0:0:0:0:0:0/124 or 127.0.0.0/28 are supported. Domain wildcards are not supported currently. Use comma, semicolon, or newline to separate multiple entries. The whitelist can hold a maximum of 4000 entries. Domains should use IDNA encoding. Ex: domain.com, 192.168.1.1, 127.0.0.0/28."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "Require all users in this group to setup Two-factor authentication"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -12133,6 +12136,9 @@ msgstr[1] ""
 | 
			
		|||
msgid "When:"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "Whitelist to allow requests to the local network from hooks and services"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "Who can see this group?"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -12912,6 +12918,9 @@ msgstr ""
 | 
			
		|||
msgid "is not an email you own"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "is too long (maximum is 1000 entries)"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "issue"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -220,53 +220,53 @@ describe Gitlab::UrlBlocker do
 | 
			
		|||
      end
 | 
			
		||||
      let(:fake_domain) { 'www.fakedomain.fake' }
 | 
			
		||||
 | 
			
		||||
      context 'true (default)' do
 | 
			
		||||
      shared_examples 'allows local requests' do |url_blocker_attributes|
 | 
			
		||||
        it 'does not block urls from private networks' do
 | 
			
		||||
          local_ips.each do |ip|
 | 
			
		||||
            stub_domain_resolv(fake_domain, ip)
 | 
			
		||||
            stub_domain_resolv(fake_domain, ip) do
 | 
			
		||||
              expect(described_class).not_to be_blocked_url("http://#{fake_domain}", url_blocker_attributes)
 | 
			
		||||
            end
 | 
			
		||||
 | 
			
		||||
            expect(described_class).not_to be_blocked_url("http://#{fake_domain}")
 | 
			
		||||
 | 
			
		||||
            unstub_domain_resolv
 | 
			
		||||
 | 
			
		||||
            expect(described_class).not_to be_blocked_url("http://#{ip}")
 | 
			
		||||
            expect(described_class).not_to be_blocked_url("http://#{ip}", url_blocker_attributes)
 | 
			
		||||
          end
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        it 'allows localhost endpoints' do
 | 
			
		||||
          expect(described_class).not_to be_blocked_url('http://0.0.0.0', allow_localhost: true)
 | 
			
		||||
          expect(described_class).not_to be_blocked_url('http://localhost', allow_localhost: true)
 | 
			
		||||
          expect(described_class).not_to be_blocked_url('http://127.0.0.1', allow_localhost: true)
 | 
			
		||||
          expect(described_class).not_to be_blocked_url('http://0.0.0.0', url_blocker_attributes)
 | 
			
		||||
          expect(described_class).not_to be_blocked_url('http://localhost', url_blocker_attributes)
 | 
			
		||||
          expect(described_class).not_to be_blocked_url('http://127.0.0.1', url_blocker_attributes)
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        it 'allows loopback endpoints' do
 | 
			
		||||
          expect(described_class).not_to be_blocked_url('http://127.0.0.2', allow_localhost: true)
 | 
			
		||||
          expect(described_class).not_to be_blocked_url('http://127.0.0.2', url_blocker_attributes)
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        it 'allows IPv4 link-local endpoints' do
 | 
			
		||||
          expect(described_class).not_to be_blocked_url('http://169.254.169.254')
 | 
			
		||||
          expect(described_class).not_to be_blocked_url('http://169.254.168.100')
 | 
			
		||||
          expect(described_class).not_to be_blocked_url('http://169.254.169.254', url_blocker_attributes)
 | 
			
		||||
          expect(described_class).not_to be_blocked_url('http://169.254.168.100', url_blocker_attributes)
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        it 'allows IPv6 link-local endpoints' do
 | 
			
		||||
          expect(described_class).not_to be_blocked_url('http://[0:0:0:0:0:ffff:169.254.169.254]')
 | 
			
		||||
          expect(described_class).not_to be_blocked_url('http://[::ffff:169.254.169.254]')
 | 
			
		||||
          expect(described_class).not_to be_blocked_url('http://[::ffff:a9fe:a9fe]')
 | 
			
		||||
          expect(described_class).not_to be_blocked_url('http://[0:0:0:0:0:ffff:169.254.168.100]')
 | 
			
		||||
          expect(described_class).not_to be_blocked_url('http://[::ffff:169.254.168.100]')
 | 
			
		||||
          expect(described_class).not_to be_blocked_url('http://[::ffff:a9fe:a864]')
 | 
			
		||||
          expect(described_class).not_to be_blocked_url('http://[fe80::c800:eff:fe74:8]')
 | 
			
		||||
          expect(described_class).not_to be_blocked_url('http://[0:0:0:0:0:ffff:169.254.169.254]', url_blocker_attributes)
 | 
			
		||||
          expect(described_class).not_to be_blocked_url('http://[::ffff:169.254.169.254]', url_blocker_attributes)
 | 
			
		||||
          expect(described_class).not_to be_blocked_url('http://[::ffff:a9fe:a9fe]', url_blocker_attributes)
 | 
			
		||||
          expect(described_class).not_to be_blocked_url('http://[0:0:0:0:0:ffff:169.254.168.100]', url_blocker_attributes)
 | 
			
		||||
          expect(described_class).not_to be_blocked_url('http://[::ffff:169.254.168.100]', url_blocker_attributes)
 | 
			
		||||
          expect(described_class).not_to be_blocked_url('http://[::ffff:a9fe:a864]', url_blocker_attributes)
 | 
			
		||||
          expect(described_class).not_to be_blocked_url('http://[fe80::c800:eff:fe74:8]', url_blocker_attributes)
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      context 'true (default)' do
 | 
			
		||||
        it_behaves_like 'allows local requests', { allow_localhost: true, allow_local_network: true }
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      context 'false' do
 | 
			
		||||
        it 'blocks urls from private networks' do
 | 
			
		||||
          local_ips.each do |ip|
 | 
			
		||||
            stub_domain_resolv(fake_domain, ip)
 | 
			
		||||
 | 
			
		||||
            stub_domain_resolv(fake_domain, ip) do
 | 
			
		||||
              expect(described_class).to be_blocked_url("http://#{fake_domain}", allow_local_network: false)
 | 
			
		||||
 | 
			
		||||
            unstub_domain_resolv
 | 
			
		||||
            end
 | 
			
		||||
 | 
			
		||||
            expect(described_class).to be_blocked_url("http://#{ip}", allow_local_network: false)
 | 
			
		||||
          end
 | 
			
		||||
| 
						 | 
				
			
			@ -286,15 +286,169 @@ describe Gitlab::UrlBlocker do
 | 
			
		|||
          expect(described_class).to be_blocked_url('http://[::ffff:a9fe:a864]', allow_local_network: false)
 | 
			
		||||
          expect(described_class).to be_blocked_url('http://[fe80::c800:eff:fe74:8]', allow_local_network: false)
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        context 'when local domain/IP is whitelisted' do
 | 
			
		||||
          let(:url_blocker_attributes) do
 | 
			
		||||
            {
 | 
			
		||||
              allow_localhost: false,
 | 
			
		||||
              allow_local_network: false
 | 
			
		||||
            }
 | 
			
		||||
          end
 | 
			
		||||
 | 
			
		||||
      def stub_domain_resolv(domain, ip)
 | 
			
		||||
          before do
 | 
			
		||||
            stub_application_setting(outbound_local_requests_whitelist: whitelist)
 | 
			
		||||
          end
 | 
			
		||||
 | 
			
		||||
          context 'with IPs in whitelist' do
 | 
			
		||||
            let(:whitelist) do
 | 
			
		||||
              [
 | 
			
		||||
                '0.0.0.0',
 | 
			
		||||
                '127.0.0.1',
 | 
			
		||||
                '127.0.0.2',
 | 
			
		||||
                '192.168.1.1',
 | 
			
		||||
                '192.168.1.2',
 | 
			
		||||
                '0:0:0:0:0:ffff:192.168.1.2',
 | 
			
		||||
                '::ffff:c0a8:102',
 | 
			
		||||
                '10.0.0.2',
 | 
			
		||||
                '0:0:0:0:0:ffff:10.0.0.2',
 | 
			
		||||
                '::ffff:a00:2',
 | 
			
		||||
                '172.16.0.2',
 | 
			
		||||
                '0:0:0:0:0:ffff:172.16.0.2',
 | 
			
		||||
                '::ffff:ac10:20',
 | 
			
		||||
                'feef::1',
 | 
			
		||||
                'fee2::',
 | 
			
		||||
                'fc00:bf8b:e62c:abcd:abcd:aaaa:aaaa:aaaa',
 | 
			
		||||
                '0:0:0:0:0:ffff:169.254.169.254',
 | 
			
		||||
                '::ffff:a9fe:a9fe',
 | 
			
		||||
                '::ffff:169.254.168.100',
 | 
			
		||||
                '::ffff:a9fe:a864',
 | 
			
		||||
                'fe80::c800:eff:fe74:8',
 | 
			
		||||
 | 
			
		||||
                # garbage IPs
 | 
			
		||||
                '45645632345',
 | 
			
		||||
                'garbage456:more345gar:bage'
 | 
			
		||||
              ]
 | 
			
		||||
            end
 | 
			
		||||
 | 
			
		||||
            it_behaves_like 'allows local requests', { allow_localhost: false, allow_local_network: false }
 | 
			
		||||
 | 
			
		||||
            it 'whitelists IP when dns_rebind_protection is disabled' do
 | 
			
		||||
              stub_domain_resolv('example.com', '192.168.1.1') do
 | 
			
		||||
                expect(described_class).not_to be_blocked_url("http://example.com",
 | 
			
		||||
                  url_blocker_attributes.merge(dns_rebind_protection: false))
 | 
			
		||||
              end
 | 
			
		||||
            end
 | 
			
		||||
          end
 | 
			
		||||
 | 
			
		||||
          context 'with domains in whitelist' do
 | 
			
		||||
            let(:whitelist) do
 | 
			
		||||
              [
 | 
			
		||||
                'www.example.com',
 | 
			
		||||
                'example.com',
 | 
			
		||||
                'xn--itlab-j1a.com',
 | 
			
		||||
                'garbage$^$%#$^&$'
 | 
			
		||||
              ]
 | 
			
		||||
            end
 | 
			
		||||
 | 
			
		||||
            it 'allows domains present in whitelist' do
 | 
			
		||||
              domain = 'example.com'
 | 
			
		||||
              subdomain1 = 'www.example.com'
 | 
			
		||||
              subdomain2 = 'subdomain.example.com'
 | 
			
		||||
 | 
			
		||||
              stub_domain_resolv(domain, '192.168.1.1') do
 | 
			
		||||
                expect(described_class).not_to be_blocked_url("http://#{domain}",
 | 
			
		||||
                  url_blocker_attributes)
 | 
			
		||||
              end
 | 
			
		||||
 | 
			
		||||
              stub_domain_resolv(subdomain1, '192.168.1.1') do
 | 
			
		||||
                expect(described_class).not_to be_blocked_url("http://#{subdomain1}",
 | 
			
		||||
                  url_blocker_attributes)
 | 
			
		||||
              end
 | 
			
		||||
 | 
			
		||||
              # subdomain2 is not part of the whitelist so it should be blocked
 | 
			
		||||
              stub_domain_resolv(subdomain2, '192.168.1.1') do
 | 
			
		||||
                expect(described_class).to be_blocked_url("http://#{subdomain2}",
 | 
			
		||||
                  url_blocker_attributes)
 | 
			
		||||
              end
 | 
			
		||||
            end
 | 
			
		||||
 | 
			
		||||
            it 'works with unicode and idna encoded domains' do
 | 
			
		||||
              unicode_domain = 'ğitlab.com'
 | 
			
		||||
              idna_encoded_domain = 'xn--itlab-j1a.com'
 | 
			
		||||
 | 
			
		||||
              stub_domain_resolv(unicode_domain, '192.168.1.1') do
 | 
			
		||||
                expect(described_class).not_to be_blocked_url("http://#{unicode_domain}",
 | 
			
		||||
                  url_blocker_attributes)
 | 
			
		||||
              end
 | 
			
		||||
 | 
			
		||||
              stub_domain_resolv(idna_encoded_domain, '192.168.1.1') do
 | 
			
		||||
                expect(described_class).not_to be_blocked_url("http://#{idna_encoded_domain}",
 | 
			
		||||
                  url_blocker_attributes)
 | 
			
		||||
              end
 | 
			
		||||
            end
 | 
			
		||||
          end
 | 
			
		||||
 | 
			
		||||
          context 'with ip ranges in whitelist' do
 | 
			
		||||
            let(:ipv4_range) { '127.0.0.0/28' }
 | 
			
		||||
            let(:ipv6_range) { 'fd84:6d02:f6d8:c89e::/124' }
 | 
			
		||||
 | 
			
		||||
            let(:whitelist) do
 | 
			
		||||
              [
 | 
			
		||||
                ipv4_range,
 | 
			
		||||
                ipv6_range
 | 
			
		||||
              ]
 | 
			
		||||
            end
 | 
			
		||||
 | 
			
		||||
            it 'blocks ipv4 range when not in whitelist' do
 | 
			
		||||
              stub_application_setting(outbound_local_requests_whitelist: [])
 | 
			
		||||
 | 
			
		||||
              IPAddr.new(ipv4_range).to_range.to_a.each do |ip|
 | 
			
		||||
                expect(described_class).to be_blocked_url("http://#{ip}",
 | 
			
		||||
                  url_blocker_attributes)
 | 
			
		||||
              end
 | 
			
		||||
            end
 | 
			
		||||
 | 
			
		||||
            it 'allows all ipv4s in the range when in whitelist' do
 | 
			
		||||
              IPAddr.new(ipv4_range).to_range.to_a.each do |ip|
 | 
			
		||||
                expect(described_class).not_to be_blocked_url("http://#{ip}",
 | 
			
		||||
                  url_blocker_attributes)
 | 
			
		||||
              end
 | 
			
		||||
            end
 | 
			
		||||
 | 
			
		||||
            it 'blocks ipv6 range when not in whitelist' do
 | 
			
		||||
              stub_application_setting(outbound_local_requests_whitelist: [])
 | 
			
		||||
 | 
			
		||||
              IPAddr.new(ipv6_range).to_range.to_a.each do |ip|
 | 
			
		||||
                expect(described_class).to be_blocked_url("http://[#{ip}]",
 | 
			
		||||
                  url_blocker_attributes)
 | 
			
		||||
              end
 | 
			
		||||
            end
 | 
			
		||||
 | 
			
		||||
            it 'allows all ipv6s in the range when in whitelist' do
 | 
			
		||||
              IPAddr.new(ipv6_range).to_range.to_a.each do |ip|
 | 
			
		||||
                expect(described_class).not_to be_blocked_url("http://[#{ip}]",
 | 
			
		||||
                  url_blocker_attributes)
 | 
			
		||||
              end
 | 
			
		||||
            end
 | 
			
		||||
 | 
			
		||||
            it 'blocks IPs outside the range' do
 | 
			
		||||
              expect(described_class).to be_blocked_url("http://[fd84:6d02:f6d8:c89e:0:0:1:f]",
 | 
			
		||||
                url_blocker_attributes)
 | 
			
		||||
 | 
			
		||||
              expect(described_class).to be_blocked_url("http://127.0.1.15",
 | 
			
		||||
                url_blocker_attributes)
 | 
			
		||||
            end
 | 
			
		||||
          end
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      def stub_domain_resolv(domain, ip, &block)
 | 
			
		||||
        address = double(ip_address: ip, ipv4_private?: true, ipv6_link_local?: false, ipv4_loopback?: false, ipv6_loopback?: false, ipv4?: false)
 | 
			
		||||
        allow(Addrinfo).to receive(:getaddrinfo).with(domain, any_args).and_return([address])
 | 
			
		||||
        allow(address).to receive(:ipv6_v4mapped?).and_return(false)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      def unstub_domain_resolv
 | 
			
		||||
        yield
 | 
			
		||||
 | 
			
		||||
        allow(Addrinfo).to receive(:getaddrinfo).and_call_original
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -231,4 +231,23 @@ describe Gitlab::Utils do
 | 
			
		|||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe '.string_to_ip_object' do
 | 
			
		||||
    it 'returns nil when string is nil' do
 | 
			
		||||
      expect(described_class.string_to_ip_object(nil)).to eq(nil)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'returns nil when string is invalid IP' do
 | 
			
		||||
      expect(described_class.string_to_ip_object('invalid ip')).to eq(nil)
 | 
			
		||||
      expect(described_class.string_to_ip_object('')).to eq(nil)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'returns IP object when string is valid IP' do
 | 
			
		||||
      expect(described_class.string_to_ip_object('192.168.1.1')).to eq(IPAddr.new('192.168.1.1'))
 | 
			
		||||
      expect(described_class.string_to_ip_object('::ffff:a9fe:a864')).to eq(IPAddr.new('::ffff:a9fe:a864'))
 | 
			
		||||
      expect(described_class.string_to_ip_object('[::ffff:a9fe:a864]')).to eq(IPAddr.new('::ffff:a9fe:a864'))
 | 
			
		||||
      expect(described_class.string_to_ip_object('127.0.0.0/28')).to eq(IPAddr.new('127.0.0.0/28'))
 | 
			
		||||
      expect(described_class.string_to_ip_object('1:0:0:0:0:0:0:0/124')).to eq(IPAddr.new('1:0:0:0:0:0:0:0/124'))
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -37,6 +37,17 @@ describe ApplicationSetting do
 | 
			
		|||
    it { is_expected.not_to allow_value("myemail@example.com").for(:lets_encrypt_notification_email) }
 | 
			
		||||
    it { is_expected.to allow_value("myemail@test.example.com").for(:lets_encrypt_notification_email) }
 | 
			
		||||
 | 
			
		||||
    it { is_expected.to allow_value(['192.168.1.1'] * 1_000).for(:outbound_local_requests_whitelist) }
 | 
			
		||||
    it { is_expected.not_to allow_value(['192.168.1.1'] * 1_001).for(:outbound_local_requests_whitelist) }
 | 
			
		||||
    it { is_expected.to allow_value(['1' * 255]).for(:outbound_local_requests_whitelist) }
 | 
			
		||||
    it { is_expected.not_to allow_value(['1' * 256]).for(:outbound_local_requests_whitelist) }
 | 
			
		||||
    it { is_expected.not_to allow_value(['ğitlab.com']).for(:outbound_local_requests_whitelist) }
 | 
			
		||||
    it { is_expected.to allow_value(['xn--itlab-j1a.com']).for(:outbound_local_requests_whitelist) }
 | 
			
		||||
    it { is_expected.not_to allow_value(['<h1></h1>']).for(:outbound_local_requests_whitelist) }
 | 
			
		||||
    it { is_expected.to allow_value(['gitlab.com']).for(:outbound_local_requests_whitelist) }
 | 
			
		||||
    it { is_expected.to allow_value(nil).for(:outbound_local_requests_whitelist) }
 | 
			
		||||
    it { is_expected.to allow_value([]).for(:outbound_local_requests_whitelist) }
 | 
			
		||||
 | 
			
		||||
    context "when user accepted let's encrypt terms of service" do
 | 
			
		||||
      before do
 | 
			
		||||
        setting.update(lets_encrypt_terms_of_service_accepted: true)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,65 +1,82 @@
 | 
			
		|||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
RSpec.shared_examples 'application settings examples' do
 | 
			
		||||
  context 'restricted signup domains' do
 | 
			
		||||
RSpec.shared_examples 'string of domains' do |attribute|
 | 
			
		||||
  it 'sets single domain' do
 | 
			
		||||
      setting.domain_whitelist_raw = 'example.com'
 | 
			
		||||
      expect(setting.domain_whitelist).to eq(['example.com'])
 | 
			
		||||
    setting.method("#{attribute}_raw=").call('example.com')
 | 
			
		||||
    expect(setting.method(attribute).call).to eq(['example.com'])
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  it 'sets multiple domains with spaces' do
 | 
			
		||||
      setting.domain_whitelist_raw = 'example.com *.example.com'
 | 
			
		||||
      expect(setting.domain_whitelist).to eq(['example.com', '*.example.com'])
 | 
			
		||||
    setting.method("#{attribute}_raw=").call('example.com *.example.com')
 | 
			
		||||
    expect(setting.method(attribute).call).to eq(['example.com', '*.example.com'])
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  it 'sets multiple domains with newlines and a space' do
 | 
			
		||||
      setting.domain_whitelist_raw = "example.com\n *.example.com"
 | 
			
		||||
      expect(setting.domain_whitelist).to eq(['example.com', '*.example.com'])
 | 
			
		||||
    setting.method("#{attribute}_raw=").call("example.com\n *.example.com")
 | 
			
		||||
    expect(setting.method(attribute).call).to eq(['example.com', '*.example.com'])
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  it 'sets multiple domains with commas' do
 | 
			
		||||
      setting.domain_whitelist_raw = "example.com, *.example.com"
 | 
			
		||||
      expect(setting.domain_whitelist).to eq(['example.com', '*.example.com'])
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  context 'blacklisted signup domains' do
 | 
			
		||||
    it 'sets single domain' do
 | 
			
		||||
      setting.domain_blacklist_raw = 'example.com'
 | 
			
		||||
      expect(setting.domain_blacklist).to contain_exactly('example.com')
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'sets multiple domains with spaces' do
 | 
			
		||||
      setting.domain_blacklist_raw = 'example.com *.example.com'
 | 
			
		||||
      expect(setting.domain_blacklist).to contain_exactly('example.com', '*.example.com')
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'sets multiple domains with newlines and a space' do
 | 
			
		||||
      setting.domain_blacklist_raw = "example.com\n *.example.com"
 | 
			
		||||
      expect(setting.domain_blacklist).to contain_exactly('example.com', '*.example.com')
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'sets multiple domains with commas' do
 | 
			
		||||
      setting.domain_blacklist_raw = "example.com, *.example.com"
 | 
			
		||||
      expect(setting.domain_blacklist).to contain_exactly('example.com', '*.example.com')
 | 
			
		||||
    setting.method("#{attribute}_raw=").call("example.com, *.example.com")
 | 
			
		||||
    expect(setting.method(attribute).call).to eq(['example.com', '*.example.com'])
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  it 'sets multiple domains with semicolon' do
 | 
			
		||||
      setting.domain_blacklist_raw = "example.com; *.example.com"
 | 
			
		||||
      expect(setting.domain_blacklist).to contain_exactly('example.com', '*.example.com')
 | 
			
		||||
    setting.method("#{attribute}_raw=").call("example.com; *.example.com")
 | 
			
		||||
    expect(setting.method(attribute).call).to contain_exactly('example.com', '*.example.com')
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  it 'sets multiple domains with mixture of everything' do
 | 
			
		||||
      setting.domain_blacklist_raw = "example.com; *.example.com\n test.com\sblock.com   yes.com"
 | 
			
		||||
      expect(setting.domain_blacklist).to contain_exactly('example.com', '*.example.com', 'test.com', 'block.com', 'yes.com')
 | 
			
		||||
    setting.method("#{attribute}_raw=").call("example.com; *.example.com\n test.com\sblock.com   yes.com")
 | 
			
		||||
    expect(setting.method(attribute).call).to contain_exactly('example.com', '*.example.com', 'test.com', 'block.com', 'yes.com')
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  it 'removes duplicates' do
 | 
			
		||||
    setting.method("#{attribute}_raw=").call("example.com; example.com; 127.0.0.1; 127.0.0.1")
 | 
			
		||||
    expect(setting.method(attribute).call).to contain_exactly('example.com', '127.0.0.1')
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  it 'does not fail with garbage values' do
 | 
			
		||||
    setting.method("#{attribute}_raw=").call("example;34543:garbage:fdh5654;")
 | 
			
		||||
    expect(setting.method(attribute).call).to contain_exactly('example', '34543:garbage:fdh5654')
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
RSpec.shared_examples 'application settings examples' do
 | 
			
		||||
  context 'restricted signup domains' do
 | 
			
		||||
    it_behaves_like 'string of domains', :domain_whitelist
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  context 'blacklisted signup domains' do
 | 
			
		||||
    it_behaves_like 'string of domains', :domain_blacklist
 | 
			
		||||
 | 
			
		||||
    it 'sets multiple domain with file' do
 | 
			
		||||
      setting.domain_blacklist_file = File.open(Rails.root.join('spec/fixtures/', 'domain_blacklist.txt'))
 | 
			
		||||
      expect(setting.domain_blacklist).to contain_exactly('example.com', 'test.com', 'foo.bar')
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  context 'outbound_local_requests_whitelist' do
 | 
			
		||||
    it_behaves_like 'string of domains', :outbound_local_requests_whitelist
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  context 'outbound_local_requests_whitelist_arrays' do
 | 
			
		||||
    it 'separates the IPs and domains' do
 | 
			
		||||
      setting.outbound_local_requests_whitelist = [
 | 
			
		||||
        '192.168.1.1', '127.0.0.0/28', 'www.example.com', 'example.com',
 | 
			
		||||
        '::ffff:a00:2', '1:0:0:0:0:0:0:0/124', 'subdomain.example.com'
 | 
			
		||||
      ]
 | 
			
		||||
 | 
			
		||||
      ip_whitelist = [
 | 
			
		||||
        IPAddr.new('192.168.1.1'), IPAddr.new('127.0.0.0/8'),
 | 
			
		||||
        IPAddr.new('::ffff:a00:2'), IPAddr.new('1:0:0:0:0:0:0:0/124')
 | 
			
		||||
      ]
 | 
			
		||||
      domain_whitelist = ['www.example.com', 'example.com', 'subdomain.example.com']
 | 
			
		||||
 | 
			
		||||
      expect(setting.outbound_local_requests_whitelist_arrays).to contain_exactly(ip_whitelist, domain_whitelist)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe 'usage ping settings' do
 | 
			
		||||
    context 'when the usage ping is disabled in gitlab.yml' do
 | 
			
		||||
      before do
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue