summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFrancisco Javier López <fjlopez@gitlab.com>2018-12-27 08:36:57 +0000
committerJohn Jarvis <jarv@gitlab.com>2018-12-27 08:36:57 +0000
commitd48d1c753bc6a4ef43cd7b6c6ccde4ff2fb4137d (patch)
tree5be91e9fdb50fa92ee0e491c36ffbaa86998d42c
parent5c7a254f8827d70e510d91ac05c1620db60513c2 (diff)
downloadgitlab-ce-d48d1c753bc6a4ef43cd7b6c6ccde4ff2fb4137d.tar.gz
[11.6] SSRF in project imports with LFS
-rw-r--r--app/services/projects/lfs_pointers/lfs_download_service.rb36
-rw-r--r--spec/services/projects/lfs_pointers/lfs_download_service_spec.rb65
2 files changed, 88 insertions, 13 deletions
diff --git a/app/services/projects/lfs_pointers/lfs_download_service.rb b/app/services/projects/lfs_pointers/lfs_download_service.rb
index 1c4a8d05be6..b5128443435 100644
--- a/app/services/projects/lfs_pointers/lfs_download_service.rb
+++ b/app/services/projects/lfs_pointers/lfs_download_service.rb
@@ -4,33 +4,51 @@
module Projects
module LfsPointers
class LfsDownloadService < BaseService
+ VALID_PROTOCOLS = %w[http https].freeze
+
# rubocop: disable CodeReuse/ActiveRecord
def execute(oid, url)
return unless project&.lfs_enabled? && oid.present? && url.present?
return if LfsObject.exists?(oid: oid)
- sanitized_uri = Gitlab::UrlSanitizer.new(url)
+ sanitized_uri = sanitize_url!(url)
with_tmp_file(oid) do |file|
- size = download_and_save_file(file, sanitized_uri)
- lfs_object = LfsObject.new(oid: oid, size: size, file: file)
+ download_and_save_file(file, sanitized_uri)
+ lfs_object = LfsObject.new(oid: oid, size: file.size, file: file)
project.all_lfs_objects << lfs_object
end
+ rescue Gitlab::UrlBlocker::BlockedUrlError => e
+ Rails.logger.error("LFS file with oid #{oid} couldn't be downloaded: #{e.message}")
rescue StandardError => e
- Rails.logger.error("LFS file with oid #{oid} could't be downloaded from #{sanitized_uri.sanitized_url}: #{e.message}")
+ Rails.logger.error("LFS file with oid #{oid} couldn't be downloaded from #{sanitized_uri.sanitized_url}: #{e.message}")
end
# rubocop: enable CodeReuse/ActiveRecord
private
+ def sanitize_url!(url)
+ Gitlab::UrlSanitizer.new(url).tap do |sanitized_uri|
+ # Just validate that HTTP/HTTPS protocols are used. The
+ # subsequent Gitlab::HTTP.get call will do network checks
+ # based on the settings.
+ Gitlab::UrlBlocker.validate!(sanitized_uri.sanitized_url,
+ protocols: VALID_PROTOCOLS)
+ end
+ end
+
def download_and_save_file(file, sanitized_uri)
- IO.copy_stream(open(sanitized_uri.sanitized_url, headers(sanitized_uri)), file) # rubocop:disable Security/Open
+ response = Gitlab::HTTP.get(sanitized_uri.sanitized_url, headers(sanitized_uri)) do |fragment|
+ file.write(fragment)
+ end
+
+ raise StandardError, "Received error code #{response.code}" unless response.success?
end
def headers(sanitized_uri)
- {}.tap do |headers|
+ query_options.tap do |headers|
credentials = sanitized_uri.credentials
if credentials[:user].present? || credentials[:password].present?
@@ -40,10 +58,14 @@ module Projects
end
end
+ def query_options
+ { stream_body: true }
+ end
+
def with_tmp_file(oid)
create_tmp_storage_dir
- File.open(File.join(tmp_storage_dir, oid), 'w') { |file| yield file }
+ File.open(File.join(tmp_storage_dir, oid), 'wb') { |file| yield file }
end
def create_tmp_storage_dir
diff --git a/spec/services/projects/lfs_pointers/lfs_download_service_spec.rb b/spec/services/projects/lfs_pointers/lfs_download_service_spec.rb
index 6af5bfc7689..95c9b6e63b8 100644
--- a/spec/services/projects/lfs_pointers/lfs_download_service_spec.rb
+++ b/spec/services/projects/lfs_pointers/lfs_download_service_spec.rb
@@ -4,17 +4,15 @@ describe Projects::LfsPointers::LfsDownloadService do
let(:project) { create(:project) }
let(:oid) { '9e548e25631dd9ce6b43afd6359ab76da2819d6a5b474e66118c7819e1d8b3e8' }
let(:download_link) { "http://gitlab.com/#{oid}" }
- let(:lfs_content) do
- <<~HEREDOC
- whatever
- HEREDOC
- end
+ let(:lfs_content) { SecureRandom.random_bytes(10) }
subject { described_class.new(project) }
before do
allow(project).to receive(:lfs_enabled?).and_return(true)
WebMock.stub_request(:get, download_link).to_return(body: lfs_content)
+
+ allow(Gitlab::CurrentSettings).to receive(:allow_local_requests_from_hooks_and_services?).and_return(false)
end
describe '#execute' do
@@ -32,7 +30,7 @@ describe Projects::LfsPointers::LfsDownloadService do
it 'stores the content' do
subject.execute(oid, download_link)
- expect(File.read(LfsObject.first.file.file.file)).to eq lfs_content
+ expect(File.binread(LfsObject.first.file.file.file)).to eq lfs_content
end
end
@@ -54,6 +52,61 @@ describe Projects::LfsPointers::LfsDownloadService do
end
end
+ context 'when localhost requests are allowed' do
+ let(:download_link) { 'http://192.168.2.120' }
+
+ before do
+ allow(Gitlab::CurrentSettings).to receive(:allow_local_requests_from_hooks_and_services?).and_return(true)
+ end
+
+ it 'downloads the file' do
+ expect(subject).to receive(:download_and_save_file).and_call_original
+
+ expect { subject.execute(oid, download_link) }.to change { LfsObject.count }.by(1)
+ end
+ end
+
+ context 'when a bad URL is used' do
+ where(download_link: ['/etc/passwd', 'ftp://example.com', 'http://127.0.0.2', 'http://192.168.2.120'])
+
+ with_them do
+ it 'does not download the file' do
+ expect { subject.execute(oid, download_link) }.not_to change { LfsObject.count }
+ end
+ end
+ end
+
+ context 'when the URL points to a redirected URL' do
+ context 'that is blocked' do
+ where(redirect_link: ['ftp://example.com', 'http://127.0.0.2', 'http://192.168.2.120'])
+
+ with_them do
+ before do
+ WebMock.stub_request(:get, download_link).to_return(status: 301, headers: { 'Location' => redirect_link })
+ end
+
+ it 'does not follow the redirection' do
+ expect(Rails.logger).to receive(:error).with(/LFS file with oid #{oid} couldn't be downloaded/)
+
+ expect { subject.execute(oid, download_link) }.not_to change { LfsObject.count }
+ end
+ end
+ end
+
+ context 'that is valid' do
+ let(:redirect_link) { "http://example.com/"}
+
+ before do
+ WebMock.stub_request(:get, download_link).to_return(status: 301, headers: { 'Location' => redirect_link })
+ WebMock.stub_request(:get, redirect_link).to_return(body: lfs_content)
+ end
+
+ it 'follows the redirection' do
+ expect { subject.execute(oid, download_link) }.to change { LfsObject.count }.from(0).to(1)
+ end
+ end
+ end
+
context 'when an lfs object with the same oid already exists' do
before do
create(:lfs_object, oid: 'oid')