Merge branch 'security-11-5-fix-webhook-ssrf-ipv6' into 'security-11-5'
[11.5] Fix SSRF in project integrations See merge request gitlab/gitlabhq!2611
This commit is contained in:
parent
82f455a8ca
commit
a9f5b22394
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix SSRF in project integrations
|
||||
merge_request:
|
||||
author:
|
||||
type: security
|
|
@ -1,6 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'resolv'
|
||||
require 'ipaddress'
|
||||
|
||||
module Gitlab
|
||||
class UrlBlocker
|
||||
|
@ -23,7 +24,9 @@ module Gitlab
|
|||
validate_hostname!(uri.hostname)
|
||||
|
||||
begin
|
||||
addrs_info = Addrinfo.getaddrinfo(uri.hostname, port, nil, :STREAM)
|
||||
addrs_info = Addrinfo.getaddrinfo(uri.hostname, port, nil, :STREAM).map do |addr|
|
||||
addr.ipv6_v4mapped? ? addr.ipv6_to_ipv4 : addr
|
||||
end
|
||||
rescue SocketError
|
||||
return true
|
||||
end
|
||||
|
@ -82,13 +85,14 @@ module Gitlab
|
|||
|
||||
def validate_hostname!(value)
|
||||
return if value.blank?
|
||||
return if IPAddress.valid?(value)
|
||||
return if value =~ /\A\p{Alnum}/
|
||||
|
||||
raise BlockedUrlError, "Hostname needs to start with an alphanumeric character"
|
||||
raise BlockedUrlError, "Hostname or IP address invalid"
|
||||
end
|
||||
|
||||
def validate_localhost!(addrs_info)
|
||||
local_ips = ["127.0.0.1", "::1", "0.0.0.0"]
|
||||
local_ips = ["::", "0.0.0.0"]
|
||||
local_ips.concat(Socket.ip_address_list.map(&:ip_address))
|
||||
|
||||
return if (local_ips & addrs_info.map(&:ip_address)).empty?
|
||||
|
@ -103,7 +107,7 @@ module Gitlab
|
|||
end
|
||||
|
||||
def validate_local_network!(addrs_info)
|
||||
return unless addrs_info.any? { |addr| addr.ipv4_private? || addr.ipv6_sitelocal? }
|
||||
return unless addrs_info.any? { |addr| addr.ipv4_private? || addr.ipv6_sitelocal? || addr.ipv6_unique_local? }
|
||||
|
||||
raise BlockedUrlError, "Requests to the local network are not allowed"
|
||||
end
|
||||
|
|
|
@ -38,23 +38,37 @@ describe Gitlab::UrlBlocker do
|
|||
end
|
||||
|
||||
it 'returns true for localhost IPs' do
|
||||
expect(described_class.blocked_url?('https://[0:0:0:0:0:0:0:0]/foo/foo.git')).to be true
|
||||
expect(described_class.blocked_url?('https://0.0.0.0/foo/foo.git')).to be true
|
||||
expect(described_class.blocked_url?('https://[::1]/foo/foo.git')).to be true
|
||||
expect(described_class.blocked_url?('https://127.0.0.1/foo/foo.git')).to be true
|
||||
expect(described_class.blocked_url?('https://[::]/foo/foo.git')).to be true
|
||||
end
|
||||
|
||||
it 'returns true for loopback IP' do
|
||||
expect(described_class.blocked_url?('https://127.0.0.2/foo/foo.git')).to be true
|
||||
expect(described_class.blocked_url?('https://127.0.0.1/foo/foo.git')).to be true
|
||||
expect(described_class.blocked_url?('https://[::1]/foo/foo.git')).to be true
|
||||
end
|
||||
|
||||
it 'returns true for alternative version of 127.0.0.1 (0177.1)' do
|
||||
expect(described_class.blocked_url?('https://0177.1:65535/foo/foo.git')).to be true
|
||||
end
|
||||
|
||||
it 'returns true for alternative version of 127.0.0.1 (017700000001)' do
|
||||
expect(described_class.blocked_url?('https://017700000001:65535/foo/foo.git')).to be true
|
||||
end
|
||||
|
||||
it 'returns true for alternative version of 127.0.0.1 (0x7f.1)' do
|
||||
expect(described_class.blocked_url?('https://0x7f.1:65535/foo/foo.git')).to be true
|
||||
end
|
||||
|
||||
it 'returns true for alternative version of 127.0.0.1 (0x7f.0.0.1)' do
|
||||
expect(described_class.blocked_url?('https://0x7f.0.0.1:65535/foo/foo.git')).to be true
|
||||
end
|
||||
|
||||
it 'returns true for alternative version of 127.0.0.1 (0x7f000001)' do
|
||||
expect(described_class.blocked_url?('https://0x7f000001:65535/foo/foo.git')).to be true
|
||||
end
|
||||
|
||||
it 'returns true for alternative version of 127.0.0.1 (2130706433)' do
|
||||
expect(described_class.blocked_url?('https://2130706433:65535/foo/foo.git')).to be true
|
||||
end
|
||||
|
@ -63,6 +77,27 @@ describe Gitlab::UrlBlocker do
|
|||
expect(described_class.blocked_url?('https://127.000.000.001:65535/foo/foo.git')).to be true
|
||||
end
|
||||
|
||||
it 'returns true for alternative version of 127.0.0.1 (127.0.1)' do
|
||||
expect(described_class.blocked_url?('https://127.0.1:65535/foo/foo.git')).to be true
|
||||
end
|
||||
|
||||
context 'with ipv6 mapped address' do
|
||||
it 'returns true for localhost IPs' do
|
||||
expect(described_class.blocked_url?('https://[0:0:0:0:0:ffff:0.0.0.0]/foo/foo.git')).to be true
|
||||
expect(described_class.blocked_url?('https://[::ffff:0.0.0.0]/foo/foo.git')).to be true
|
||||
expect(described_class.blocked_url?('https://[::ffff:0:0]/foo/foo.git')).to be true
|
||||
end
|
||||
|
||||
it 'returns true for loopback IPs' do
|
||||
expect(described_class.blocked_url?('https://[0:0:0:0:0:ffff:127.0.0.1]/foo/foo.git')).to be true
|
||||
expect(described_class.blocked_url?('https://[::ffff:127.0.0.1]/foo/foo.git')).to be true
|
||||
expect(described_class.blocked_url?('https://[::ffff:7f00:1]/foo/foo.git')).to be true
|
||||
expect(described_class.blocked_url?('https://[0:0:0:0:0:ffff:127.0.0.2]/foo/foo.git')).to be true
|
||||
expect(described_class.blocked_url?('https://[::ffff:127.0.0.2]/foo/foo.git')).to be true
|
||||
expect(described_class.blocked_url?('https://[::ffff:7f00:2]/foo/foo.git')).to be true
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns true for a non-alphanumeric hostname' do
|
||||
stub_resolv
|
||||
|
||||
|
@ -86,7 +121,22 @@ describe Gitlab::UrlBlocker do
|
|||
end
|
||||
|
||||
context 'when allow_local_network is' do
|
||||
let(:local_ips) { ['192.168.1.2', '10.0.0.2', '172.16.0.2'] }
|
||||
let(:local_ips) do
|
||||
[
|
||||
'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]'
|
||||
]
|
||||
end
|
||||
let(:fake_domain) { 'www.fakedomain.fake' }
|
||||
|
||||
context 'true (default)' do
|
||||
|
@ -117,10 +167,14 @@ describe Gitlab::UrlBlocker do
|
|||
expect(described_class).not_to be_blocked_url('http://169.254.168.100')
|
||||
end
|
||||
|
||||
# This is blocked due to the hostname check: https://gitlab.com/gitlab-org/gitlab-ce/issues/50227
|
||||
it 'blocks IPv6 link-local endpoints' do
|
||||
expect(described_class).to be_blocked_url('http://[::ffff:169.254.169.254]')
|
||||
expect(described_class).to be_blocked_url('http://[::ffff:169.254.168.100]')
|
||||
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]')
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -143,14 +197,20 @@ describe Gitlab::UrlBlocker do
|
|||
end
|
||||
|
||||
it 'blocks IPv6 link-local endpoints' do
|
||||
expect(described_class).to be_blocked_url('http://[0:0:0:0:0:ffff:169.254.169.254]', allow_local_network: false)
|
||||
expect(described_class).to be_blocked_url('http://[::ffff:169.254.169.254]', allow_local_network: false)
|
||||
expect(described_class).to be_blocked_url('http://[::ffff:a9fe:a9fe]', allow_local_network: false)
|
||||
expect(described_class).to be_blocked_url('http://[0:0:0:0:0:ffff:169.254.168.100]', allow_local_network: false)
|
||||
expect(described_class).to be_blocked_url('http://[::ffff:169.254.168.100]', 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://[::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
|
||||
end
|
||||
|
||||
def stub_domain_resolv(domain, ip)
|
||||
allow(Addrinfo).to receive(:getaddrinfo).with(domain, any_args).and_return([double(ip_address: ip, ipv4_private?: true, ipv6_link_local?: false, ipv4_loopback?: false, ipv6_loopback?: false)])
|
||||
address = double(ip_address: ip, ipv4_private?: true, ipv6_link_local?: false, ipv4_loopback?: false, ipv6_loopback?: 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
|
||||
|
@ -191,6 +251,36 @@ describe Gitlab::UrlBlocker do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#validate_hostname!' do
|
||||
let(:ip_addresses) do
|
||||
[
|
||||
'2001:db8:1f70::999:de8:7648:6e8',
|
||||
'FE80::C800:EFF:FE74:8',
|
||||
'::ffff:127.0.0.1',
|
||||
'::ffff:169.254.168.100',
|
||||
'::ffff:7f00:1',
|
||||
'0:0:0:0:0:ffff:0.0.0.0',
|
||||
'localhost',
|
||||
'127.0.0.1',
|
||||
'127.000.000.001',
|
||||
'0x7f000001',
|
||||
'0x7f.0.0.1',
|
||||
'0x7f.0.0.1',
|
||||
'017700000001',
|
||||
'0177.1',
|
||||
'2130706433',
|
||||
'::',
|
||||
'::1'
|
||||
]
|
||||
end
|
||||
|
||||
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
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Resolv does not support resolving UTF-8 domain names
|
||||
# See https://bugs.ruby-lang.org/issues/4270
|
||||
def stub_resolv
|
||||
|
|
Loading…
Reference in New Issue