New upstream version 12.10.2

This commit is contained in:
Pirate Praveen 2020-05-01 12:34:13 +05:30
parent 33be1af3d8
commit d76068268a
14 changed files with 337 additions and 52 deletions

View file

@ -1,5 +1,12 @@
Please view this file on the master branch, on stable branches it's out of date. Please view this file on the master branch, on stable branches it's out of date.
## 12.10.1 (2020-04-24)
### Changed (1 change)
- Move project deploy tokens section back to Repository settings. !29280
## 12.10.0 (2020-04-22) ## 12.10.0 (2020-04-22)
### Fixed (6 changes, 1 of them is from the community) ### Fixed (6 changes, 1 of them is from the community)

View file

@ -2,6 +2,20 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. entry.
## 12.10.2 (2020-04-30)
### Security (8 changes)
- Ensure MR diff exists before codeowner check.
- Apply CODEOWNERS validations to web requests.
- Prevent unauthorized access to default branch.
- Do not return private project ID without permission.
- Fix doorkeeper CVE-2020-10187.
- Change GitHub service integration token input to password.
- Return only safe urls for mirrors.
- Validate workhorse 'rewritten_fields' and properly use them during multipart uploads.
## 12.10.1 (2020-04-24) ## 12.10.1 (2020-04-24)
### Fixed (5 changes) ### Fixed (5 changes)

View file

@ -1 +1 @@
12.10.1 12.10.2

View file

@ -1 +1 @@
12.10.1 12.10.2

View file

@ -5,6 +5,13 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio
layout 'profile' layout 'profile'
def index
respond_to do |format|
format.html { render "errors/not_found", layout: "errors", status: :not_found }
format.json { render json: "", status: :not_found }
end
end
def destroy def destroy
if params[:token_id].present? if params[:token_id].present?
current_resource_owner.oauth_authorized_tokens.find(params[:token_id]).revoke current_resource_owner.oauth_authorized_tokens.find(params[:token_id]).revoke

View file

@ -624,6 +624,7 @@ module ProjectsHelper
def find_file_path def find_file_path
return unless @project && !@project.empty_repo? return unless @project && !@project.empty_repo?
return unless can?(current_user, :download_code, @project)
ref = @ref || @project.repository.root_ref ref = @ref || @project.repository.root_ref

View file

@ -2,7 +2,7 @@
class RemoteMirrorEntity < Grape::Entity class RemoteMirrorEntity < Grape::Entity
expose :id expose :id
expose :url expose :safe_url, as: :url
expose :enabled expose :enabled
expose :auth_method expose :auth_method

View file

@ -43,11 +43,13 @@ module Gitlab
raise "unexpected field: #{field.inspect}" unless parsed_field.count == 1 raise "unexpected field: #{field.inspect}" unless parsed_field.count == 1
key, value = parsed_field.first key, value = parsed_field.first
if value.nil? if value.nil? # we have a top level param, eg. field = 'foo' and not 'foo[bar]'
value = open_file(@request.params, key) raise "invalid field: #{field.inspect}" if field != key
value = open_file(@request.params, key, tmp_path.presence)
@open_files << value @open_files << value
else else
value = decorate_params_value(value, @request.params[key]) value = decorate_params_value(value, @request.params[key], tmp_path.presence)
end end
update_param(key, value) update_param(key, value)
@ -59,7 +61,7 @@ module Gitlab
end end
# This function calls itself recursively # This function calls itself recursively
def decorate_params_value(path_hash, value_hash) def decorate_params_value(path_hash, value_hash, path_override = nil)
unless path_hash.is_a?(Hash) && path_hash.count == 1 unless path_hash.is_a?(Hash) && path_hash.count == 1
raise "invalid path: #{path_hash.inspect}" raise "invalid path: #{path_hash.inspect}"
end end
@ -72,19 +74,19 @@ module Gitlab
case path_value case path_value
when nil when nil
value_hash[path_key] = open_file(value_hash.dig(path_key), '') value_hash[path_key] = open_file(value_hash.dig(path_key), '', path_override)
@open_files << value_hash[path_key] @open_files << value_hash[path_key]
value_hash value_hash
when Hash when Hash
decorate_params_value(path_value, value_hash[path_key]) decorate_params_value(path_value, value_hash[path_key], path_override)
value_hash value_hash
else else
raise "unexpected path value: #{path_value.inspect}" raise "unexpected path value: #{path_value.inspect}"
end end
end end
def open_file(params, key) def open_file(params, key, path_override = nil)
::UploadedFile.from_params(params, key, allowed_paths) ::UploadedFile.from_params(params, key, allowed_paths, path_override)
end end
# update_params ensures that both rails controllers and rack middleware can find # update_params ensures that both rails controllers and rack middleware can find

View file

@ -42,13 +42,14 @@ class UploadedFile
@remote_id = remote_id @remote_id = remote_id
end end
def self.from_params(params, field, upload_paths) def self.from_params(params, field, upload_paths, path_override = nil)
path = params["#{field}.path"] path = path_override || params["#{field}.path"]
remote_id = params["#{field}.remote_id"] remote_id = params["#{field}.remote_id"]
return if path.blank? && remote_id.blank? return if path.blank? && remote_id.blank?
file_path = nil if remote_id.present? # don't use file_path if remote_id is set
if path.present? file_path = nil
elsif path.present?
file_path = File.realpath(path) file_path = File.realpath(path)
paths = Array(upload_paths) << Dir.tmpdir paths = Array(upload_paths) << Dir.tmpdir

View file

@ -0,0 +1,21 @@
# frozen_string_literal: true
require 'spec_helper'
describe Oauth::AuthorizedApplicationsController do
let(:user) { create(:user) }
let(:guest) { create(:user) }
let(:application) { create(:oauth_application, owner: guest) }
before do
sign_in(user)
end
describe 'GET #index' do
it 'responds with 404' do
get :index
expect(response).to have_gitlab_http_status(:not_found)
end
end
end

View file

@ -277,11 +277,16 @@ describe ApplicationHelper do
end end
context 'when @project is set' do context 'when @project is set' do
it 'includes all possible body data elements and associates the project elements with project' do let_it_be(:project) { create(:project, :repository) }
project = create(:project) let_it_be(:user) { create(:user) }
before do
assign(:project, project) assign(:project, project)
allow(helper).to receive(:current_user).and_return(nil)
end
it 'includes all possible body data elements and associates the project elements with project' do
expect(helper).to receive(:can?).with(nil, :download_code, project)
expect(helper.body_data).to eq( expect(helper.body_data).to eq(
{ {
page: 'application', page: 'application',
@ -302,12 +307,11 @@ describe ApplicationHelper do
context 'when params[:id] is present and the issue exsits and action_name is show' do context 'when params[:id] is present and the issue exsits and action_name is show' do
it 'sets all project and id elements correctly related to the issue' do it 'sets all project and id elements correctly related to the issue' do
issue = create(:issue) issue = create(:issue, project: project)
stub_controller_method(:action_name, 'show') stub_controller_method(:action_name, 'show')
stub_controller_method(:params, { id: issue.id }) stub_controller_method(:params, { id: issue.id })
assign(:project, issue.project) expect(helper).to receive(:can?).with(nil, :download_code, project).and_return(false)
expect(helper.body_data).to eq( expect(helper.body_data).to eq(
{ {
page: 'projects:issues:show', page: 'projects:issues:show',
@ -322,6 +326,15 @@ describe ApplicationHelper do
end end
end end
end end
context 'when current_user has download_code permission' do
it 'returns find_file with the default branch' do
allow(helper).to receive(:current_user).and_return(user)
expect(helper).to receive(:can?).with(user, :download_code, project).and_return(true)
expect(helper.body_data[:find_file]).to end_with(project.default_branch)
end
end
end end
def stub_controller_method(method_name, value) def stub_controller_method(method_name, value)

View file

@ -7,11 +7,11 @@ require 'tempfile'
describe Gitlab::Middleware::Multipart do describe Gitlab::Middleware::Multipart do
include_context 'multipart middleware context' include_context 'multipart middleware context'
shared_examples_for 'multipart upload files' do RSpec.shared_examples_for 'multipart upload files' do
it 'opens top-level files' do it 'opens top-level files' do
Tempfile.open('top-level') do |tempfile| Tempfile.open('top-level') do |tempfile|
rewritten = { 'file' => tempfile.path } rewritten = { 'file' => tempfile.path }
in_params = { 'file.name' => original_filename, 'file.path' => tempfile.path, 'file.remote_id' => remote_id } in_params = { 'file.name' => original_filename, 'file.path' => file_path, 'file.remote_id' => remote_id, 'file.size' => file_size }
env = post_env(rewritten, in_params, Gitlab::Workhorse.secret, 'gitlab-workhorse') env = post_env(rewritten, in_params, Gitlab::Workhorse.secret, 'gitlab-workhorse')
expect_uploaded_file(tempfile, %w(file)) expect_uploaded_file(tempfile, %w(file))
@ -22,8 +22,8 @@ describe Gitlab::Middleware::Multipart do
it 'opens files one level deep' do it 'opens files one level deep' do
Tempfile.open('one-level') do |tempfile| Tempfile.open('one-level') do |tempfile|
in_params = { 'user' => { 'avatar' => { '.name' => original_filename, '.path' => tempfile.path, '.remote_id' => remote_id } } }
rewritten = { 'user[avatar]' => tempfile.path } rewritten = { 'user[avatar]' => tempfile.path }
in_params = { 'user' => { 'avatar' => { '.name' => original_filename, '.path' => file_path, '.remote_id' => remote_id, '.size' => file_size } } }
env = post_env(rewritten, in_params, Gitlab::Workhorse.secret, 'gitlab-workhorse') env = post_env(rewritten, in_params, Gitlab::Workhorse.secret, 'gitlab-workhorse')
expect_uploaded_file(tempfile, %w(user avatar)) expect_uploaded_file(tempfile, %w(user avatar))
@ -34,7 +34,7 @@ describe Gitlab::Middleware::Multipart do
it 'opens files two levels deep' do it 'opens files two levels deep' do
Tempfile.open('two-levels') do |tempfile| Tempfile.open('two-levels') do |tempfile|
in_params = { 'project' => { 'milestone' => { 'themesong' => { '.name' => original_filename, '.path' => tempfile.path, '.remote_id' => remote_id } } } } in_params = { 'project' => { 'milestone' => { 'themesong' => { '.name' => original_filename, '.path' => file_path, '.remote_id' => remote_id, '.size' => file_size } } } }
rewritten = { 'project[milestone][themesong]' => tempfile.path } rewritten = { 'project[milestone][themesong]' => tempfile.path }
env = post_env(rewritten, in_params, Gitlab::Workhorse.secret, 'gitlab-workhorse') env = post_env(rewritten, in_params, Gitlab::Workhorse.secret, 'gitlab-workhorse')
@ -44,13 +44,61 @@ describe Gitlab::Middleware::Multipart do
end end
end end
def expect_uploaded_file(tempfile, path, remote: false) def expect_uploaded_file(tempfile, path)
expect(app).to receive(:call) do |env| expect(app).to receive(:call) do |env|
file = get_params(env).dig(*path) file = get_params(env).dig(*path)
expect(file).to be_a(::UploadedFile) expect(file).to be_a(::UploadedFile)
expect(file.path).to eq(tempfile.path)
expect(file.original_filename).to eq(original_filename) expect(file.original_filename).to eq(original_filename)
expect(file.remote_id).to eq(remote_id)
if remote_id
expect(file.remote_id).to eq(remote_id)
expect(file.path).to be_nil
else
expect(file.path).to eq(File.realpath(tempfile.path))
expect(file.remote_id).to be_nil
end
end
end
end
RSpec.shared_examples_for 'handling CI artifact upload' do
it 'uploads both file and metadata' do
Tempfile.open('file') do |file|
Tempfile.open('metadata') do |metadata|
rewritten = { 'file' => file.path, 'metadata' => metadata.path }
in_params = { 'file.name' => 'file.txt', 'file.path' => file_path, 'file.remote_id' => file_remote_id, 'file.size' => file_size, 'metadata.name' => 'metadata.gz' }
env = post_env(rewritten, in_params, Gitlab::Workhorse.secret, 'gitlab-workhorse')
with_expected_uploaded_artifact_files(file, metadata) do |uploaded_file, uploaded_metadata|
expect(uploaded_file).to be_a(::UploadedFile)
expect(uploaded_file.original_filename).to eq('file.txt')
if file_remote_id
expect(uploaded_file.remote_id).to eq(file_remote_id)
expect(uploaded_file.size).to eq(file_size)
expect(uploaded_file.path).to be_nil
else
expect(uploaded_file.path).to eq(File.realpath(file.path))
expect(uploaded_file.remote_id).to be_nil
end
expect(uploaded_metadata).to be_a(::UploadedFile)
expect(uploaded_metadata.original_filename).to eq('metadata.gz')
expect(uploaded_metadata.path).to eq(File.realpath(metadata.path))
expect(uploaded_metadata.remote_id).to be_nil
end
middleware.call(env)
end
end
end
def with_expected_uploaded_artifact_files(file, metadata)
expect(app).to receive(:call) do |env|
file = get_params(env).dig('file')
metadata = get_params(env).dig('metadata')
yield file, metadata
end end
end end
end end
@ -67,18 +115,65 @@ describe Gitlab::Middleware::Multipart do
expect { middleware.call(env) }.to raise_error(JWT::InvalidIssuerError) expect { middleware.call(env) }.to raise_error(JWT::InvalidIssuerError)
end end
context 'with invalid rewritten field' do
invalid_field_names = [
'[file]',
';file',
'file]',
';file]',
'file]]',
'file;;'
]
invalid_field_names.each do |invalid_field_name|
it "rejects invalid rewritten field name #{invalid_field_name}" do
env = post_env({ invalid_field_name => nil }, {}, Gitlab::Workhorse.secret, 'gitlab-workhorse')
expect { middleware.call(env) }.to raise_error(RuntimeError, "invalid field: \"#{invalid_field_name}\"")
end
end
end
context 'with remote file' do context 'with remote file' do
let(:remote_id) { 'someid' } let(:remote_id) { 'someid' }
let(:file_size) { 300 }
let(:file_path) { '' }
it_behaves_like 'multipart upload files'
end
context 'with remote file and a file path set' do
let(:remote_id) { 'someid' }
let(:file_size) { 300 }
let(:file_path) { 'not_a_valid_file_path' } # file path will come from the rewritten_fields
it_behaves_like 'multipart upload files' it_behaves_like 'multipart upload files'
end end
context 'with local file' do context 'with local file' do
let(:remote_id) { nil } let(:remote_id) { nil }
let(:file_size) { nil }
let(:file_path) { 'not_a_valid_file_path' } # file path will come from the rewritten_fields
it_behaves_like 'multipart upload files' it_behaves_like 'multipart upload files'
end end
context 'with remote CI artifact upload' do
let(:file_remote_id) { 'someid' }
let(:file_size) { 300 }
let(:file_path) { 'not_a_valid_file_path' } # file path will come from the rewritten_fields
it_behaves_like 'handling CI artifact upload'
end
context 'with local CI artifact upload' do
let(:file_remote_id) { nil }
let(:file_size) { nil }
let(:file_path) { 'not_a_valid_file_path' } # file path will come from the rewritten_fields
it_behaves_like 'handling CI artifact upload'
end
it 'allows files in uploads/tmp directory' do it 'allows files in uploads/tmp directory' do
with_tmp_dir('public/uploads/tmp') do |dir, env| with_tmp_dir('public/uploads/tmp') do |dir, env|
expect(app).to receive(:call) do |env| expect(app).to receive(:call) do |env|

View file

@ -4,7 +4,7 @@ require 'spec_helper'
describe UploadedFile do describe UploadedFile do
let(:temp_dir) { Dir.tmpdir } let(:temp_dir) { Dir.tmpdir }
let(:temp_file) { Tempfile.new("test", temp_dir) } let(:temp_file) { Tempfile.new(%w[test test], temp_dir) }
before do before do
FileUtils.touch(temp_file) FileUtils.touch(temp_file)
@ -16,13 +16,14 @@ describe UploadedFile do
describe ".from_params" do describe ".from_params" do
let(:upload_path) { nil } let(:upload_path) { nil }
let(:file_path_override) { nil }
after do after do
FileUtils.rm_r(upload_path) if upload_path FileUtils.rm_r(upload_path) if upload_path
end end
subject do subject do
described_class.from_params(params, :file, upload_path) described_class.from_params(params, :file, upload_path, file_path_override)
end end
context 'when valid file is specified' do context 'when valid file is specified' do
@ -31,9 +32,7 @@ describe UploadedFile do
{ 'file.path' => temp_file.path } { 'file.path' => temp_file.path }
end end
it "succeeds" do it { is_expected.not_to be_nil }
is_expected.not_to be_nil
end
it "generates filename from path" do it "generates filename from path" do
expect(subject.original_filename).to eq(::File.basename(temp_file.path)) expect(subject.original_filename).to eq(::File.basename(temp_file.path))
@ -41,33 +40,153 @@ describe UploadedFile do
end end
context 'all parameters are specified' do context 'all parameters are specified' do
let(:params) do RSpec.shared_context 'filepath override' do
{ 'file.path' => temp_file.path, let(:temp_file_override) { Tempfile.new(%w[override override], temp_dir) }
'file.name' => 'dir/my file&.txt', let(:file_path_override) { temp_file_override.path }
'file.type' => 'my/type',
'file.sha256' => 'sha256', before do
'file.remote_id' => 'remote_id' } FileUtils.touch(temp_file_override)
end
after do
FileUtils.rm_f(temp_file_override)
end
end end
it "succeeds" do RSpec.shared_examples 'using the file path' do |filename:, content_type:, sha256:, path_suffix:|
is_expected.not_to be_nil it 'sets properly the attributes' do
expect(subject.original_filename).to eq(filename)
expect(subject.content_type).to eq(content_type)
expect(subject.sha256).to eq(sha256)
expect(subject.remote_id).to be_nil
expect(subject.path).to end_with(path_suffix)
end
it 'handles a blank path' do
params['file.path'] = ''
# Not a real file, so can't determine size itself
params['file.size'] = 1.byte
expect { described_class.from_params(params, :file, upload_path) }
.not_to raise_error
end
end end
it "generates filename from path" do RSpec.shared_examples 'using the remote id' do |filename:, content_type:, sha256:, size:, remote_id:|
expect(subject.original_filename).to eq('my_file_.txt') it 'sets properly the attributes' do
expect(subject.content_type).to eq('my/type') expect(subject.original_filename).to eq(filename)
expect(subject.sha256).to eq('sha256') expect(subject.content_type).to eq('application/octet-stream')
expect(subject.remote_id).to eq('remote_id') expect(subject.sha256).to eq('sha256')
expect(subject.path).to be_nil
expect(subject.size).to eq(123456)
expect(subject.remote_id).to eq('1234567890')
end
end end
it 'handles a blank path' do context 'with a filepath' do
params['file.path'] = '' let(:params) do
{ 'file.path' => temp_file.path,
'file.name' => 'dir/my file&.txt',
'file.type' => 'my/type',
'file.sha256' => 'sha256' }
end
# Not a real file, so can't determine size itself it { is_expected.not_to be_nil }
params['file.size'] = 1.byte
expect { described_class.from_params(params, :file, upload_path) } it_behaves_like 'using the file path',
.not_to raise_error filename: 'my_file_.txt',
content_type: 'my/type',
sha256: 'sha256',
path_suffix: 'test'
end
context 'with a filepath override' do
include_context 'filepath override'
let(:params) do
{ 'file.path' => temp_file.path,
'file.name' => 'dir/my file&.txt',
'file.type' => 'my/type',
'file.sha256' => 'sha256' }
end
it { is_expected.not_to be_nil }
it_behaves_like 'using the file path',
filename: 'my_file_.txt',
content_type: 'my/type',
sha256: 'sha256',
path_suffix: 'override'
end
context 'with a remote id' do
let(:params) do
{
'file.name' => 'dir/my file&.txt',
'file.sha256' => 'sha256',
'file.remote_url' => 'http://localhost/file',
'file.remote_id' => '1234567890',
'file.etag' => 'etag1234567890',
'file.size' => '123456'
}
end
it { is_expected.not_to be_nil }
it_behaves_like 'using the remote id',
filename: 'my_file_.txt',
content_type: 'application/octet-stream',
sha256: 'sha256',
size: 123456,
remote_id: '1234567890'
end
context 'with a path and a remote id' do
let(:params) do
{
'file.path' => temp_file.path,
'file.name' => 'dir/my file&.txt',
'file.sha256' => 'sha256',
'file.remote_url' => 'http://localhost/file',
'file.remote_id' => '1234567890',
'file.etag' => 'etag1234567890',
'file.size' => '123456'
}
end
it { is_expected.not_to be_nil }
it_behaves_like 'using the remote id',
filename: 'my_file_.txt',
content_type: 'application/octet-stream',
sha256: 'sha256',
size: 123456,
remote_id: '1234567890'
end
context 'with a path override and a remote id' do
include_context 'filepath override'
let(:params) do
{
'file.name' => 'dir/my file&.txt',
'file.sha256' => 'sha256',
'file.remote_url' => 'http://localhost/file',
'file.remote_id' => '1234567890',
'file.etag' => 'etag1234567890',
'file.size' => '123456'
}
end
it { is_expected.not_to be_nil }
it_behaves_like 'using the remote id',
filename: 'my_file_.txt',
content_type: 'application/octet-stream',
sha256: 'sha256',
size: 123456,
remote_id: '1234567890'
end end
end end
end end

View file

@ -3,7 +3,7 @@
require 'spec_helper' require 'spec_helper'
describe RemoteMirrorEntity do describe RemoteMirrorEntity do
let(:project) { create(:project, :repository, :remote_mirror) } let(:project) { create(:project, :repository, :remote_mirror, url: "https://test:password@gitlab.com") }
let(:remote_mirror) { project.remote_mirrors.first } let(:remote_mirror) { project.remote_mirrors.first }
let(:entity) { described_class.new(remote_mirror) } let(:entity) { described_class.new(remote_mirror) }
@ -15,4 +15,9 @@ describe RemoteMirrorEntity do
:ssh_known_hosts, :ssh_public_key, :ssh_known_hosts_fingerprints :ssh_known_hosts, :ssh_public_key, :ssh_known_hosts_fingerprints
) )
end end
it 'does not expose password information' do
expect(subject[:url]).not_to include('password')
expect(subject[:url]).to eq(remote_mirror.safe_url)
end
end end