summaryrefslogtreecommitdiff
path: root/spec/lib/gitlab/url_blocker_spec.rb
diff options
context:
space:
mode:
Diffstat (limited to 'spec/lib/gitlab/url_blocker_spec.rb')
-rw-r--r--spec/lib/gitlab/url_blocker_spec.rb260
1 files changed, 218 insertions, 42 deletions
diff --git a/spec/lib/gitlab/url_blocker_spec.rb b/spec/lib/gitlab/url_blocker_spec.rb
index 253366e0789..df8a1f82f81 100644
--- a/spec/lib/gitlab/url_blocker_spec.rb
+++ b/spec/lib/gitlab/url_blocker_spec.rb
@@ -1,4 +1,6 @@
# coding: utf-8
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::UrlBlocker do
@@ -68,6 +70,16 @@ describe Gitlab::UrlBlocker do
expect(uri).to eq(Addressable::URI.parse('https://example.org'))
expect(hostname).to eq(nil)
end
+
+ context 'when it cannot be resolved' do
+ let(:import_url) { 'http://foobar.x' }
+
+ it 'raises error' do
+ stub_env('RSPEC_ALLOW_INVALID_URLS', 'false')
+
+ expect { described_class.validate!(import_url) }.to raise_error(described_class::BlockedUrlError)
+ end
+ end
end
context 'when the URL hostname is an IP address' do
@@ -79,6 +91,16 @@ describe Gitlab::UrlBlocker do
expect(uri).to eq(Addressable::URI.parse('https://93.184.216.34'))
expect(hostname).to be(nil)
end
+
+ context 'when it is invalid' do
+ let(:import_url) { 'http://1.1.1.1.1' }
+
+ it 'raises an error' do
+ stub_env('RSPEC_ALLOW_INVALID_URLS', 'false')
+
+ expect { described_class.validate!(import_url) }.to raise_error(described_class::BlockedUrlError)
+ end
+ end
end
end
end
@@ -180,8 +202,6 @@ describe Gitlab::UrlBlocker do
end
it 'returns true for a non-alphanumeric hostname' do
- stub_resolv
-
aggregate_failures do
expect(described_class).to be_blocked_url('ssh://-oProxyCommand=whoami/a')
@@ -220,53 +240,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)
-
- expect(described_class).not_to be_blocked_url("http://#{fake_domain}")
+ stub_domain_resolv(fake_domain, ip) do
+ expect(described_class).not_to be_blocked_url("http://#{fake_domain}", url_blocker_attributes)
+ end
- 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)
-
- expect(described_class).to be_blocked_url("http://#{fake_domain}", allow_local_network: false)
-
- unstub_domain_resolv
+ stub_domain_resolv(fake_domain, ip) do
+ expect(described_class).to be_blocked_url("http://#{fake_domain}", allow_local_network: false)
+ end
expect(described_class).to be_blocked_url("http://#{ip}", allow_local_network: false)
end
@@ -286,24 +306,174 @@ 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
+
+ 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)
+ 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
context 'when enforce_user is' do
- before do
- stub_resolv
- end
-
context 'false (default)' do
it 'does not block urls with a non-alphanumeric username' do
expect(described_class).not_to be_blocked_url('ssh://-oProxyCommand=whoami@example.com/a')
@@ -351,9 +521,21 @@ describe Gitlab::UrlBlocker do
expect(described_class.blocked_url?('https://git‌lab.com/foo/foo.bar', ascii_only: true)).to be true
end
end
+
+ it 'blocks urls with invalid ip address' do
+ stub_env('RSPEC_ALLOW_INVALID_URLS', 'false')
+
+ expect(described_class).to be_blocked_url('http://8.8.8.8.8')
+ end
+
+ it 'blocks urls whose hostname cannot be resolved' do
+ stub_env('RSPEC_ALLOW_INVALID_URLS', 'false')
+
+ expect(described_class).to be_blocked_url('http://foobar.x')
+ end
end
- describe '#validate_hostname!' do
+ describe '#validate_hostname' do
let(:ip_addresses) do
[
'2001:db8:1f70::999:de8:7648:6e8',
@@ -378,14 +560,8 @@ describe Gitlab::UrlBlocker do
it 'does not raise error for valid Ip addresses' do
ip_addresses.each do |ip|
- expect { described_class.send(:validate_hostname!, ip) }.not_to raise_error
+ expect { described_class.send(:validate_hostname, ip) }.not_to raise_error
end
end
end
-
- # Resolv does not support resolving UTF-8 domain names
- # See https://bugs.ruby-lang.org/issues/4270
- def stub_resolv
- allow(Resolv).to receive(:getaddresses).and_return([])
- end
end