Commit 777a606f authored by Francisco Javier López's avatar Francisco Javier López Committed by Thong Kuah

Avoid dns rebinding checks if the domain is whitelisted

If the domain is whitelisted we skip dns rebinding checks as well
as local host checks
parent 480e284e
---
title: Avoid dns rebinding checks when the domain is whitelisted
merge_request: 32603
author:
type: changed
...@@ -45,21 +45,18 @@ module Gitlab ...@@ -45,21 +45,18 @@ module Gitlab
ascii_only: ascii_only ascii_only: ascii_only
) )
normalized_hostname = uri.normalized_host address_info = get_address_info(uri, dns_rebind_protection)
hostname = uri.hostname
port = get_port(uri)
address_info = get_address_info(hostname, port, dns_rebind_protection)
return [uri, nil] unless address_info return [uri, nil] unless address_info
ip_address = ip_address(address_info) ip_address = ip_address(address_info)
protected_uri_with_hostname = enforce_uri_hostname(ip_address, uri, hostname, dns_rebind_protection) return [uri, nil] if domain_whitelisted?(uri) || ip_whitelisted?(ip_address)
protected_uri_with_hostname = enforce_uri_hostname(ip_address, uri, 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
...@@ -86,12 +83,12 @@ module Gitlab ...@@ -86,12 +83,12 @@ 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(ip_address, uri, hostname, dns_rebind_protection) def enforce_uri_hostname(ip_address, uri, dns_rebind_protection)
return [uri, nil] unless dns_rebind_protection && ip_address && ip_address != hostname return [uri, nil] unless dns_rebind_protection && ip_address && ip_address != uri.hostname
uri = uri.dup new_uri = uri.dup
uri.hostname = ip_address new_uri.hostname = ip_address
[uri, hostname] [new_uri, uri.hostname]
end end
def ip_address(address_info) def ip_address(address_info)
...@@ -110,14 +107,14 @@ module Gitlab ...@@ -110,14 +107,14 @@ module Gitlab
validate_unicode_restriction(uri) if ascii_only validate_unicode_restriction(uri) if ascii_only
end end
def get_address_info(hostname, port, dns_rebind_protection) def get_address_info(uri, dns_rebind_protection)
Addrinfo.getaddrinfo(hostname, port, nil, :STREAM).map do |addr| Addrinfo.getaddrinfo(uri.hostname, get_port(uri), nil, :STREAM).map do |addr|
addr.ipv6_v4mapped? ? addr.ipv6_to_ipv4 : addr addr.ipv6_v4mapped? ? addr.ipv6_to_ipv4 : addr
end end
rescue SocketError rescue SocketError
# If the dns rebinding protection is not enabled, we allow # If the dns rebinding protection is not enabled or the domain
# urls that can't be resolved at this point. # is whitelisted we avoid the dns rebinding checks
return unless dns_rebind_protection return if domain_whitelisted?(uri) || !dns_rebind_protection
# In the test suite we use a lot of mocked urls that are either invalid or # In the test suite we use a lot of mocked urls that are either invalid or
# don't exist. In order to avoid modifying a ton of tests and factories # don't exist. In order to avoid modifying a ton of tests and factories
...@@ -131,18 +128,11 @@ module Gitlab ...@@ -131,18 +128,11 @@ module Gitlab
end end
def validate_local_request( def validate_local_request(
normalized_hostname:,
address_info:, address_info:,
allow_localhost:, allow_localhost:,
allow_local_network:) 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)
...@@ -258,14 +248,12 @@ module Gitlab ...@@ -258,14 +248,12 @@ 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) def domain_whitelisted?(uri)
ip_obj = Gitlab::Utils.string_to_ip_object(ip_string) Gitlab::UrlBlockers::UrlWhitelist.domain_whitelisted?(uri.normalized_host)
ip_whitelist.any? { |ip| ip.include?(ip_obj) }
end end
def local_domain_whitelisted?(domain_whitelist, domain_string) def ip_whitelisted?(ip_address)
domain_whitelist.include?(domain_string) Gitlab::UrlBlockers::UrlWhitelist.ip_whitelisted?(ip_address)
end end
def config def config
......
# frozen_string_literal: true
module Gitlab
module UrlBlockers
class UrlWhitelist
class << self
def ip_whitelisted?(ip_string)
return false if ip_string.blank?
ip_whitelist, _ = outbound_local_requests_whitelist_arrays
ip_obj = Gitlab::Utils.string_to_ip_object(ip_string)
ip_whitelist.any? { |ip| ip.include?(ip_obj) }
end
def domain_whitelisted?(domain_string)
return false if domain_string.blank?
_, domain_whitelist = outbound_local_requests_whitelist_arrays
domain_whitelist.include?(domain_string)
end
private
attr_reader :ip_whitelist, :domain_whitelist
# We cannot use Gitlab::CurrentSettings as ApplicationSetting itself
# calls this class. This ends up in a cycle where
# Gitlab::CurrentSettings creates an ApplicationSetting which then
# calls this method.
#
# See https://gitlab.com/gitlab-org/gitlab-ee/issues/9833
def outbound_local_requests_whitelist_arrays
return [[], []] unless ApplicationSetting.current
ApplicationSetting.current.outbound_local_requests_whitelist_arrays
end
end
end
end
end
...@@ -30,8 +30,12 @@ describe Gitlab::UrlBlocker do ...@@ -30,8 +30,12 @@ describe Gitlab::UrlBlocker do
context 'when URI is internal' do context 'when URI is internal' do
let(:import_url) { 'http://localhost' } let(:import_url) { 'http://localhost' }
before do
stub_dns(import_url, ip_address: '127.0.0.1')
end
it_behaves_like 'validates URI and hostname' do it_behaves_like 'validates URI and hostname' do
let(:expected_uri) { 'http://[::1]' } let(:expected_uri) { 'http://127.0.0.1' }
let(:expected_hostname) { 'localhost' } let(:expected_hostname) { 'localhost' }
end end
end end
...@@ -347,6 +351,7 @@ describe Gitlab::UrlBlocker do ...@@ -347,6 +351,7 @@ describe Gitlab::UrlBlocker do
end end
before do before do
allow(ApplicationSetting).to receive(:current).and_return(ApplicationSetting.new)
stub_application_setting(outbound_local_requests_whitelist: whitelist) stub_application_setting(outbound_local_requests_whitelist: whitelist)
end end
...@@ -384,9 +389,15 @@ describe Gitlab::UrlBlocker do ...@@ -384,9 +389,15 @@ describe Gitlab::UrlBlocker do
it_behaves_like 'allows local requests', { allow_localhost: false, allow_local_network: false } it_behaves_like 'allows local requests', { allow_localhost: false, allow_local_network: false }
it 'whitelists IP when dns_rebind_protection is disabled' do it 'whitelists IP when dns_rebind_protection is disabled' do
stub_domain_resolv('example.com', '192.168.1.1') do url = "http://example.com"
expect(described_class).not_to be_blocked_url("http://example.com", attrs = url_blocker_attributes.merge(dns_rebind_protection: false)
url_blocker_attributes.merge(dns_rebind_protection: false))
stub_domain_resolv('example.com', '192.168.1.2') do
expect(described_class).not_to be_blocked_url(url, attrs)
end
stub_domain_resolv('example.com', '192.168.1.3') do
expect(described_class).to be_blocked_url(url, attrs)
end end
end end
end end
...@@ -437,6 +448,51 @@ describe Gitlab::UrlBlocker do ...@@ -437,6 +448,51 @@ describe Gitlab::UrlBlocker do
url_blocker_attributes) url_blocker_attributes)
end end
end end
shared_examples 'dns rebinding checks' do
shared_examples 'whitelists the domain' do
let(:whitelist) { [domain] }
let(:url) { "http://#{domain}" }
before do
stub_env('RSPEC_ALLOW_INVALID_URLS', 'false')
end
it do
expect(described_class).not_to be_blocked_url(url, dns_rebind_protection: dns_rebind_value)
end
end
context 'when dns_rebinding_setting is' do
context 'enabled' do
let(:dns_rebind_value) { true }
it_behaves_like 'whitelists the domain'
end
context 'disabled' do
let(:dns_rebind_value) { false }
it_behaves_like 'whitelists the domain'
end
end
end
context 'when the domain cannot be resolved' do
let(:domain) { 'foobar.x' }
it_behaves_like 'dns rebinding checks'
end
context 'when the domain can be resolved' do
let(:domain) { 'example.com' }
before do
stub_dns(url, ip_address: '93.184.216.34')
end
it_behaves_like 'dns rebinding checks'
end
end end
context 'with ip ranges in whitelist' do context 'with ip ranges in whitelist' do
......
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::UrlBlockers::UrlWhitelist do
include StubRequests
let(:whitelist) { [] }
before do
allow(ApplicationSetting).to receive(:current).and_return(ApplicationSetting.new)
stub_application_setting(outbound_local_requests_whitelist: whitelist)
end
describe '#domain_whitelisted?' do
let(:whitelist) do
[
'www.example.com',
'example.com'
]
end
it 'returns true if domains present in whitelist' do
aggregate_failures do
whitelist.each do |domain|
expect(described_class).to be_domain_whitelisted(domain)
end
['subdomain.example.com', 'example.org'].each do |domain|
expect(described_class).not_to be_domain_whitelisted(domain)
end
end
end
it 'returns false when domain is blank' do
expect(described_class).not_to be_domain_whitelisted(nil)
end
end
describe '#ip_whitelisted?' do
let(:whitelist) do
[
'0.0.0.0',
'127.0.0.1',
'192.168.1.1',
'0:0:0:0:0:ffff:192.168.1.2',
'::ffff:c0a8:102',
'fc00:bf8b:e62c:abcd:abcd:aaaa:aaaa:aaaa',
'0:0:0:0:0:ffff:169.254.169.254',
'::ffff:a9fe:a9fe',
'::ffff:a9fe:a864',
'fe80::c800:eff:fe74:8'
]
end
it 'returns true if ips present in whitelist' do
aggregate_failures do
whitelist.each do |ip_address|
expect(described_class).to be_ip_whitelisted(ip_address)
end
['172.16.2.2', '127.0.0.2', 'fe80::c800:eff:fe74:9'].each do |ip_address|
expect(described_class).not_to be_ip_whitelisted(ip_address)
end
end
end
it 'returns false when ip is blank' do
expect(described_class).not_to be_ip_whitelisted(nil)
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment