Merge branch '44496-outbound_requests_whitelist' into 'master'
Add Outbound requests whitelist for local networks See merge request gitlab-org/gitlab-ce!30350
This commit is contained in:
commit
f73d65197b
|
|
@ -177,6 +177,7 @@ module ApplicationSettingsHelper
|
||||||
:domain_blacklist_enabled,
|
:domain_blacklist_enabled,
|
||||||
:domain_blacklist_raw,
|
:domain_blacklist_raw,
|
||||||
:domain_whitelist_raw,
|
:domain_whitelist_raw,
|
||||||
|
:outbound_local_requests_whitelist_raw,
|
||||||
:dsa_key_restriction,
|
:dsa_key_restriction,
|
||||||
:ecdsa_key_restriction,
|
:ecdsa_key_restriction,
|
||||||
:ed25519_key_restriction,
|
:ed25519_key_restriction,
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,11 @@ class ApplicationSetting < ApplicationRecord
|
||||||
|
|
||||||
validates :uuid, presence: true
|
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,
|
validates :session_expire_delay,
|
||||||
presence: true,
|
presence: true,
|
||||||
numericality: { only_integer: true, greater_than_or_equal_to: 0 }
|
numericality: { only_integer: true, greater_than_or_equal_to: 0 }
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
module ApplicationSettingImplementation
|
module ApplicationSettingImplementation
|
||||||
extend ActiveSupport::Concern
|
extend ActiveSupport::Concern
|
||||||
|
include Gitlab::Utils::StrongMemoize
|
||||||
|
|
||||||
DOMAIN_LIST_SEPARATOR = %r{\s*[,;]\s* # comma or semicolon, optionally surrounded by whitespace
|
DOMAIN_LIST_SEPARATOR = %r{\s*[,;]\s* # comma or semicolon, optionally surrounded by whitespace
|
||||||
| # or
|
| # or
|
||||||
|
|
@ -96,7 +97,8 @@ module ApplicationSettingImplementation
|
||||||
diff_max_patch_bytes: Gitlab::Git::Diff::DEFAULT_MAX_PATCH_BYTES,
|
diff_max_patch_bytes: Gitlab::Git::Diff::DEFAULT_MAX_PATCH_BYTES,
|
||||||
commit_email_hostname: default_commit_email_hostname,
|
commit_email_hostname: default_commit_email_hostname,
|
||||||
protected_ci_variables: false,
|
protected_ci_variables: false,
|
||||||
local_markdown_version: 0
|
local_markdown_version: 0,
|
||||||
|
outbound_local_requests_whitelist: []
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -131,31 +133,52 @@ module ApplicationSettingImplementation
|
||||||
end
|
end
|
||||||
|
|
||||||
def domain_whitelist_raw
|
def domain_whitelist_raw
|
||||||
self.domain_whitelist&.join("\n")
|
array_to_string(self.domain_whitelist)
|
||||||
end
|
end
|
||||||
|
|
||||||
def domain_blacklist_raw
|
def domain_blacklist_raw
|
||||||
self.domain_blacklist&.join("\n")
|
array_to_string(self.domain_blacklist)
|
||||||
end
|
end
|
||||||
|
|
||||||
def domain_whitelist_raw=(values)
|
def domain_whitelist_raw=(values)
|
||||||
self.domain_whitelist = []
|
self.domain_whitelist = domain_strings_to_array(values)
|
||||||
self.domain_whitelist = values.split(DOMAIN_LIST_SEPARATOR)
|
|
||||||
self.domain_whitelist.reject! { |d| d.empty? }
|
|
||||||
self.domain_whitelist
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def domain_blacklist_raw=(values)
|
def domain_blacklist_raw=(values)
|
||||||
self.domain_blacklist = []
|
self.domain_blacklist = domain_strings_to_array(values)
|
||||||
self.domain_blacklist = values.split(DOMAIN_LIST_SEPARATOR)
|
|
||||||
self.domain_blacklist.reject! { |d| d.empty? }
|
|
||||||
self.domain_blacklist
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def domain_blacklist_file=(file)
|
def domain_blacklist_file=(file)
|
||||||
self.domain_blacklist_raw = file.read
|
self.domain_blacklist_raw = file.read
|
||||||
end
|
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
|
def repository_storages
|
||||||
Array(read_attribute(:repository_storages))
|
Array(read_attribute(:repository_storages))
|
||||||
end
|
end
|
||||||
|
|
@ -255,6 +278,17 @@ module ApplicationSettingImplementation
|
||||||
|
|
||||||
private
|
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!
|
def ensure_uuid!
|
||||||
return if uuid?
|
return if uuid?
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,13 @@
|
||||||
= f.label :allow_local_requests_from_hooks_and_services, class: 'form-check-label' do
|
= f.label :allow_local_requests_from_hooks_and_services, class: 'form-check-label' do
|
||||||
Allow requests to the local network from hooks and services
|
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-group
|
||||||
.form-check
|
.form-check
|
||||||
= f.check_box :dns_rebinding_protection_enabled, class: 'form-check-input'
|
= 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 "lock_memberships_to_ldap", default: false, null: false
|
||||||
t.boolean "time_tracking_limit_to_hours", 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 "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 ["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 ["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"
|
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,
|
"session_expire_delay" : 10080,
|
||||||
"home_page_url" : null,
|
"home_page_url" : null,
|
||||||
"default_snippet_visibility" : "private",
|
"default_snippet_visibility" : "private",
|
||||||
|
"outbound_local_requests_whitelist": [],
|
||||||
"domain_whitelist" : [],
|
"domain_whitelist" : [],
|
||||||
"domain_blacklist_enabled" : false,
|
"domain_blacklist_enabled" : false,
|
||||||
"domain_blacklist" : [],
|
"domain_blacklist" : [],
|
||||||
|
|
@ -113,6 +114,7 @@ Example response:
|
||||||
"default_project_visibility": "internal",
|
"default_project_visibility": "internal",
|
||||||
"default_snippet_visibility": "private",
|
"default_snippet_visibility": "private",
|
||||||
"default_group_visibility": "private",
|
"default_group_visibility": "private",
|
||||||
|
"outbound_local_requests_whitelist": [],
|
||||||
"domain_whitelist": [],
|
"domain_whitelist": [],
|
||||||
"domain_blacklist_enabled" : false,
|
"domain_blacklist_enabled" : false,
|
||||||
"domain_blacklist" : [],
|
"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` | 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_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. |
|
| `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. |
|
| `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. |
|
| `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. |
|
| `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
|
ascii_only: ascii_only
|
||||||
)
|
)
|
||||||
|
|
||||||
|
normalized_hostname = uri.normalized_host
|
||||||
hostname = uri.hostname
|
hostname = uri.hostname
|
||||||
port = get_port(uri)
|
port = get_port(uri)
|
||||||
|
|
||||||
address_info = get_address_info(hostname, port)
|
address_info = get_address_info(hostname, port)
|
||||||
return [uri, nil] unless address_info
|
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
|
# Allow url from the GitLab instance itself but only for the configured hostname and ports
|
||||||
return protected_uri_with_hostname if internal?(uri)
|
return protected_uri_with_hostname if internal?(uri)
|
||||||
|
|
||||||
validate_local_request(
|
validate_local_request(
|
||||||
|
normalized_hostname: normalized_hostname,
|
||||||
address_info: address_info,
|
address_info: address_info,
|
||||||
allow_localhost: allow_localhost,
|
allow_localhost: allow_localhost,
|
||||||
allow_local_network: allow_local_network
|
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
|
# 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.
|
# 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)
|
def enforce_uri_hostname(ip_address, uri, hostname, dns_rebind_protection)
|
||||||
address = addrs_info.first
|
|
||||||
ip_address = address&.ip_address
|
|
||||||
|
|
||||||
return [uri, nil] unless dns_rebind_protection && ip_address && ip_address != hostname
|
return [uri, nil] unless dns_rebind_protection && ip_address && ip_address != hostname
|
||||||
|
|
||||||
uri = uri.dup
|
uri = uri.dup
|
||||||
|
|
@ -94,6 +94,10 @@ module Gitlab
|
||||||
[uri, hostname]
|
[uri, hostname]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def ip_address(address_info)
|
||||||
|
address_info.first&.ip_address
|
||||||
|
end
|
||||||
|
|
||||||
def validate_uri(uri:, schemes:, ports:, enforce_sanitization:, enforce_user:, ascii_only:)
|
def validate_uri(uri:, schemes:, ports:, enforce_sanitization:, enforce_user:, ascii_only:)
|
||||||
validate_html_tags(uri) if enforce_sanitization
|
validate_html_tags(uri) if enforce_sanitization
|
||||||
|
|
||||||
|
|
@ -113,9 +117,19 @@ module Gitlab
|
||||||
rescue SocketError
|
rescue SocketError
|
||||||
end
|
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
|
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
|
unless allow_localhost
|
||||||
validate_localhost(address_info)
|
validate_localhost(address_info)
|
||||||
validate_loopback(address_info)
|
validate_loopback(address_info)
|
||||||
|
|
@ -231,6 +245,16 @@ module Gitlab
|
||||||
(uri.port.blank? || uri.port == config.gitlab_shell.ssh_port)
|
(uri.port.blank? || uri.port == config.gitlab_shell.ssh_port)
|
||||||
end
|
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
|
def config
|
||||||
Gitlab.config
|
Gitlab.config
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -131,5 +131,12 @@ module Gitlab
|
||||||
data
|
data
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def string_to_ip_object(str)
|
||||||
|
return unless str
|
||||||
|
|
||||||
|
IPAddr.new(str)
|
||||||
|
rescue IPAddr::InvalidAddressError
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -8998,6 +8998,9 @@ msgstr ""
|
||||||
msgid "Requests Profiles"
|
msgid "Requests Profiles"
|
||||||
msgstr ""
|
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"
|
msgid "Require all users in this group to setup Two-factor authentication"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
@ -12133,6 +12136,9 @@ msgstr[1] ""
|
||||||
msgid "When:"
|
msgid "When:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Whitelist to allow requests to the local network from hooks and services"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Who can see this group?"
|
msgid "Who can see this group?"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
@ -12912,6 +12918,9 @@ msgstr ""
|
||||||
msgid "is not an email you own"
|
msgid "is not an email you own"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "is too long (maximum is 1000 entries)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "issue"
|
msgid "issue"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -220,53 +220,53 @@ describe Gitlab::UrlBlocker do
|
||||||
end
|
end
|
||||||
let(:fake_domain) { 'www.fakedomain.fake' }
|
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
|
it 'does not block urls from private networks' do
|
||||||
local_ips.each do |ip|
|
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}")
|
expect(described_class).not_to be_blocked_url("http://#{ip}", url_blocker_attributes)
|
||||||
|
|
||||||
unstub_domain_resolv
|
|
||||||
|
|
||||||
expect(described_class).not_to be_blocked_url("http://#{ip}")
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'allows localhost endpoints' do
|
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://0.0.0.0', url_blocker_attributes)
|
||||||
expect(described_class).not_to be_blocked_url('http://localhost', allow_localhost: true)
|
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', allow_localhost: true)
|
expect(described_class).not_to be_blocked_url('http://127.0.0.1', url_blocker_attributes)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'allows loopback endpoints' do
|
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
|
end
|
||||||
|
|
||||||
it 'allows IPv4 link-local endpoints' do
|
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.169.254', url_blocker_attributes)
|
||||||
expect(described_class).not_to be_blocked_url('http://169.254.168.100')
|
expect(described_class).not_to be_blocked_url('http://169.254.168.100', url_blocker_attributes)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'allows IPv6 link-local endpoints' do
|
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://[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]')
|
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]')
|
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]')
|
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]')
|
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]')
|
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]')
|
expect(described_class).not_to be_blocked_url('http://[fe80::c800:eff:fe74:8]', url_blocker_attributes)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'true (default)' do
|
||||||
|
it_behaves_like 'allows local requests', { allow_localhost: true, allow_local_network: true }
|
||||||
|
end
|
||||||
|
|
||||||
context 'false' do
|
context 'false' do
|
||||||
it 'blocks urls from private networks' do
|
it 'blocks urls from private networks' do
|
||||||
local_ips.each do |ip|
|
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)
|
||||||
expect(described_class).to be_blocked_url("http://#{fake_domain}", allow_local_network: false)
|
end
|
||||||
|
|
||||||
unstub_domain_resolv
|
|
||||||
|
|
||||||
expect(described_class).to be_blocked_url("http://#{ip}", allow_local_network: false)
|
expect(described_class).to be_blocked_url("http://#{ip}", allow_local_network: false)
|
||||||
end
|
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://[::ffff:a9fe:a864]', allow_local_network: false)
|
||||||
expect(described_class).to be_blocked_url('http://[fe80::c800:eff:fe74:8]', allow_local_network: false)
|
expect(described_class).to be_blocked_url('http://[fe80::c800:eff:fe74:8]', allow_local_network: false)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when local domain/IP is whitelisted' do
|
||||||
|
let(:url_blocker_attributes) do
|
||||||
|
{
|
||||||
|
allow_localhost: false,
|
||||||
|
allow_local_network: false
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
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
|
end
|
||||||
|
|
||||||
def stub_domain_resolv(domain, ip)
|
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)
|
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(Addrinfo).to receive(:getaddrinfo).with(domain, any_args).and_return([address])
|
||||||
allow(address).to receive(:ipv6_v4mapped?).and_return(false)
|
allow(address).to receive(:ipv6_v4mapped?).and_return(false)
|
||||||
end
|
|
||||||
|
|
||||||
def unstub_domain_resolv
|
yield
|
||||||
|
|
||||||
allow(Addrinfo).to receive(:getaddrinfo).and_call_original
|
allow(Addrinfo).to receive(:getaddrinfo).and_call_original
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -231,4 +231,23 @@ describe Gitlab::Utils do
|
||||||
end
|
end
|
||||||
end
|
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
|
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.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("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
|
context "when user accepted let's encrypt terms of service" do
|
||||||
before do
|
before do
|
||||||
setting.update(lets_encrypt_terms_of_service_accepted: true)
|
setting.update(lets_encrypt_terms_of_service_accepted: true)
|
||||||
|
|
|
||||||
|
|
@ -1,58 +1,54 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
RSpec.shared_examples 'string of domains' do |attribute|
|
||||||
|
it 'sets single domain' do
|
||||||
|
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.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.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.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.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.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
|
RSpec.shared_examples 'application settings examples' do
|
||||||
context 'restricted signup domains' do
|
context 'restricted signup domains' do
|
||||||
it 'sets single domain' do
|
it_behaves_like 'string of domains', :domain_whitelist
|
||||||
setting.domain_whitelist_raw = 'example.com'
|
|
||||||
expect(setting.domain_whitelist).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'])
|
|
||||||
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'])
|
|
||||||
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
|
end
|
||||||
|
|
||||||
context 'blacklisted signup domains' do
|
context 'blacklisted signup domains' do
|
||||||
it 'sets single domain' do
|
it_behaves_like 'string of domains', :domain_blacklist
|
||||||
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')
|
|
||||||
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')
|
|
||||||
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')
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'sets multiple domain with file' do
|
it 'sets multiple domain with file' do
|
||||||
setting.domain_blacklist_file = File.open(Rails.root.join('spec/fixtures/', 'domain_blacklist.txt'))
|
setting.domain_blacklist_file = File.open(Rails.root.join('spec/fixtures/', 'domain_blacklist.txt'))
|
||||||
|
|
@ -60,6 +56,27 @@ RSpec.shared_examples 'application settings examples' do
|
||||||
end
|
end
|
||||||
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
|
describe 'usage ping settings' do
|
||||||
context 'when the usage ping is disabled in gitlab.yml' do
|
context 'when the usage ping is disabled in gitlab.yml' do
|
||||||
before do
|
before do
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue