Commit bb3469c8 authored by GitLab Release Tools Bot's avatar GitLab Release Tools Bot

Merge branch 'security-workhorse-package-bypass' into 'master'

Verify workhorse request for file uploads

See merge request gitlab-org/security/gitlab!83
parents 4345a414 043c6649
---
title: Add workhorse request verification to package upload endpoints
merge_request:
author:
type: security
......@@ -179,10 +179,7 @@ module API
end
route_setting :authentication, job_token_allowed: true
put ':id/packages/maven/*path/:file_name/authorize', requirements: MAVEN_ENDPOINT_REQUIREMENTS do
authorize_create_package!(user_project)
require_gitlab_workhorse!
Gitlab::Workhorse.verify_api_request!(headers)
authorize_upload!
status 200
content_type Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE
......@@ -205,8 +202,7 @@ module API
end
route_setting :authentication, job_token_allowed: true
put ':id/packages/maven/*path/:file_name', requirements: MAVEN_ENDPOINT_REQUIREMENTS do
authorize_create_package!(user_project)
require_gitlab_workhorse!
authorize_upload!
file_name, format = extract_format(params[:file_name])
......
......@@ -586,6 +586,8 @@ describe API::ConanPackages do
context 'without a file from workhorse' do
let(:params) { { file: nil } }
it_behaves_like 'package workhorse uploads'
it 'rejects the request' do
subject
......@@ -710,7 +712,7 @@ describe API::ConanPackages do
subject
expect(response).to have_gitlab_http_status(:error)
expect(response).to have_gitlab_http_status(:forbidden)
end
context 'when using remote storage' do
......
......@@ -10,8 +10,8 @@ describe API::MavenPackages do
let(:package_file) { package.package_files.where('file_name like ?', '%.xml').first }
let(:jar_file) { package.package_files.where('file_name like ?', '%.jar').first }
let(:personal_access_token) { create(:personal_access_token, user: user) }
let(:jwt_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') }
let(:headers) { { 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => jwt_token } }
let(:workhorse_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') }
let(:headers) { { 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => workhorse_token } }
let(:headers_with_token) { headers.merge('Private-Token' => personal_access_token.token) }
let(:job) { create(:ci_build, user: user) }
let(:version) { '1.0-SNAPSHOT' }
......@@ -40,14 +40,14 @@ describe API::MavenPackages do
it 'returns the file' do
subject
expect(response).to have_gitlab_http_status(200)
expect(response).to have_gitlab_http_status(:ok)
expect(response.content_type.to_s).to eq('application/octet-stream')
end
it 'returns sha1 of the file' do
download_file(package_file.file_name + '.sha1')
expect(response).to have_gitlab_http_status(200)
expect(response).to have_gitlab_http_status(:ok)
expect(response.content_type.to_s).to eq('text/plain')
expect(response.body).to eq(package_file.file_sha1)
end
......@@ -66,20 +66,20 @@ describe API::MavenPackages do
it 'returns the file' do
subject
expect(response).to have_gitlab_http_status(200)
expect(response).to have_gitlab_http_status(:ok)
expect(response.content_type.to_s).to eq('application/octet-stream')
end
it 'denies download when no private token' do
download_file(package_file.file_name)
expect(response).to have_gitlab_http_status(403)
expect(response).to have_gitlab_http_status(:forbidden)
end
it 'allows download with job token' do
download_file(package_file.file_name, job_token: job.token)
expect(response).to have_gitlab_http_status(200)
expect(response).to have_gitlab_http_status(:ok)
expect(response.content_type.to_s).to eq('application/octet-stream')
end
end
......@@ -96,7 +96,7 @@ describe API::MavenPackages do
it 'returns the file' do
subject
expect(response).to have_gitlab_http_status(200)
expect(response).to have_gitlab_http_status(:ok)
expect(response.content_type.to_s).to eq('application/octet-stream')
end
......@@ -105,19 +105,19 @@ describe API::MavenPackages do
subject
expect(response).to have_gitlab_http_status(403)
expect(response).to have_gitlab_http_status(:forbidden)
end
it 'denies download when no private token' do
download_file(package_file.file_name)
expect(response).to have_gitlab_http_status(403)
expect(response).to have_gitlab_http_status(:forbidden)
end
it 'allows download with job token' do
download_file(package_file.file_name, job_token: job.token)
expect(response).to have_gitlab_http_status(200)
expect(response).to have_gitlab_http_status(:ok)
expect(response.content_type.to_s).to eq('application/octet-stream')
end
end
......@@ -127,7 +127,7 @@ describe API::MavenPackages do
download_file(package_file.file_name)
expect(response).to have_gitlab_http_status(403)
expect(response).to have_gitlab_http_status(:forbidden)
end
context 'project name is different from a package name' do
......@@ -136,7 +136,7 @@ describe API::MavenPackages do
it 'rejects request' do
download_file(package_file.file_name)
expect(response).to have_gitlab_http_status(403)
expect(response).to have_gitlab_http_status(:forbidden)
end
end
......@@ -163,14 +163,14 @@ describe API::MavenPackages do
it 'returns the file' do
subject
expect(response).to have_gitlab_http_status(200)
expect(response).to have_gitlab_http_status(:ok)
expect(response.content_type.to_s).to eq('application/octet-stream')
end
it 'returns sha1 of the file' do
download_file(package_file.file_name + '.sha1')
expect(response).to have_gitlab_http_status(200)
expect(response).to have_gitlab_http_status(:ok)
expect(response.content_type.to_s).to eq('text/plain')
expect(response.body).to eq(package_file.file_sha1)
end
......@@ -189,20 +189,20 @@ describe API::MavenPackages do
it 'returns the file' do
subject
expect(response).to have_gitlab_http_status(200)
expect(response).to have_gitlab_http_status(:ok)
expect(response.content_type.to_s).to eq('application/octet-stream')
end
it 'denies download when no private token' do
download_file(package_file.file_name)
expect(response).to have_gitlab_http_status(404)
expect(response).to have_gitlab_http_status(:not_found)
end
it 'allows download with job token' do
download_file(package_file.file_name, job_token: job.token)
expect(response).to have_gitlab_http_status(200)
expect(response).to have_gitlab_http_status(:ok)
expect(response.content_type.to_s).to eq('application/octet-stream')
end
end
......@@ -219,7 +219,7 @@ describe API::MavenPackages do
it 'returns the file' do
subject
expect(response).to have_gitlab_http_status(200)
expect(response).to have_gitlab_http_status(:ok)
expect(response.content_type.to_s).to eq('application/octet-stream')
end
......@@ -228,19 +228,19 @@ describe API::MavenPackages do
subject
expect(response).to have_gitlab_http_status(403)
expect(response).to have_gitlab_http_status(:forbidden)
end
it 'denies download when no private token' do
download_file(package_file.file_name)
expect(response).to have_gitlab_http_status(404)
expect(response).to have_gitlab_http_status(:not_found)
end
it 'allows download with job token' do
download_file(package_file.file_name, job_token: job.token)
expect(response).to have_gitlab_http_status(200)
expect(response).to have_gitlab_http_status(:ok)
expect(response.content_type.to_s).to eq('application/octet-stream')
end
end
......@@ -250,7 +250,7 @@ describe API::MavenPackages do
download_file(package_file.file_name)
expect(response).to have_gitlab_http_status(403)
expect(response).to have_gitlab_http_status(:forbidden)
end
def download_file(file_name, params = {}, request_headers = headers)
......@@ -271,14 +271,14 @@ describe API::MavenPackages do
it 'returns the file' do
subject
expect(response).to have_gitlab_http_status(200)
expect(response).to have_gitlab_http_status(:ok)
expect(response.content_type.to_s).to eq('application/octet-stream')
end
it 'returns sha1 of the file' do
download_file(package_file.file_name + '.sha1')
expect(response).to have_gitlab_http_status(200)
expect(response).to have_gitlab_http_status(:ok)
expect(response.content_type.to_s).to eq('text/plain')
expect(response.body).to eq(package_file.file_sha1)
end
......@@ -296,7 +296,7 @@ describe API::MavenPackages do
it 'returns the file' do
subject
expect(response).to have_gitlab_http_status(200)
expect(response).to have_gitlab_http_status(:ok)
expect(response.content_type.to_s).to eq('application/octet-stream')
end
......@@ -305,19 +305,19 @@ describe API::MavenPackages do
subject
expect(response).to have_gitlab_http_status(403)
expect(response).to have_gitlab_http_status(:forbidden)
end
it 'denies download when no private token' do
download_file(package_file.file_name)
expect(response).to have_gitlab_http_status(404)
expect(response).to have_gitlab_http_status(:not_found)
end
it 'allows download with job token' do
download_file(package_file.file_name, job_token: job.token)
expect(response).to have_gitlab_http_status(200)
expect(response).to have_gitlab_http_status(:ok)
expect(response.content_type.to_s).to eq('application/octet-stream')
end
end
......@@ -327,7 +327,7 @@ describe API::MavenPackages do
download_file(package_file.file_name)
expect(response).to have_gitlab_http_status(403)
expect(response).to have_gitlab_http_status(:forbidden)
end
def download_file(file_name, params = {}, request_headers = headers)
......@@ -344,13 +344,13 @@ describe API::MavenPackages do
it 'rejects a malicious request' do
put api("/projects/#{project.id}/packages/maven/com/example/my-app/#{version}/%2e%2e%2F.ssh%2Fauthorized_keys/authorize"), params: {}, headers: headers_with_token
expect(response).to have_gitlab_http_status(400)
expect(response).to have_gitlab_http_status(:bad_request)
end
it 'authorizes posting package with a valid token' do
authorize_upload_with_token
expect(response).to have_gitlab_http_status(200)
expect(response).to have_gitlab_http_status(:ok)
expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
expect(json_response['TempPath']).not_to be_nil
end
......@@ -360,7 +360,7 @@ describe API::MavenPackages do
authorize_upload_with_token
expect(response).to have_gitlab_http_status(401)
expect(response).to have_gitlab_http_status(:unauthorized)
end
it 'rejects request without a valid permission' do
......@@ -368,7 +368,7 @@ describe API::MavenPackages do
authorize_upload_with_token
expect(response).to have_gitlab_http_status(403)
expect(response).to have_gitlab_http_status(:forbidden)
end
it 'rejects requests that did not go through gitlab-workhorse' do
......@@ -376,13 +376,13 @@ describe API::MavenPackages do
authorize_upload_with_token
expect(response).to have_gitlab_http_status(500)
expect(response).to have_gitlab_http_status(:forbidden)
end
it 'authorizes upload with job token' do
authorize_upload(job_token: job.token)
expect(response).to have_gitlab_http_status(200)
expect(response).to have_gitlab_http_status(:ok)
end
def authorize_upload(params = {}, request_headers = headers)
......@@ -405,13 +405,13 @@ describe API::MavenPackages do
it 'rejects requests without a file from workhorse' do
upload_file_with_token
expect(response).to have_gitlab_http_status(400)
expect(response).to have_gitlab_http_status(:bad_request)
end
it 'rejects request without a token' do
upload_file
expect(response).to have_gitlab_http_status(401)
expect(response).to have_gitlab_http_status(:unauthorized)
end
it 'rejects request if feature is not in the license' do
......@@ -419,7 +419,7 @@ describe API::MavenPackages do
upload_file_with_token
expect(response).to have_gitlab_http_status(403)
expect(response).to have_gitlab_http_status(:forbidden)
end
context 'when params from workhorse are correct' do
......@@ -435,7 +435,13 @@ describe API::MavenPackages do
it 'rejects a malicious request' do
put api("/projects/#{project.id}/packages/maven/com/example/my-app/#{version}/%2e%2e%2f.ssh%2fauthorized_keys"), params: params, headers: headers_with_token
expect(response).to have_gitlab_http_status(400)
expect(response).to have_gitlab_http_status(:bad_request)
end
context 'without workhorse header' do
subject { upload_file_with_token(params) }
it_behaves_like 'package workhorse uploads'
end
context 'event tracking' do
......@@ -451,14 +457,14 @@ describe API::MavenPackages do
.and change { Packages::MavenMetadatum.count }.by(1)
.and change { Packages::PackageFile.count }.by(1)
expect(response).to have_gitlab_http_status(200)
expect(response).to have_gitlab_http_status(:ok)
expect(package_file.file_name).to eq(file_upload.original_filename)
end
it 'allows upload with job token' do
upload_file(params.merge(job_token: job.token))
expect(response).to have_gitlab_http_status(200)
expect(response).to have_gitlab_http_status(:ok)
expect(package.build_info.pipeline).to eq job.pipeline
end
......@@ -468,7 +474,7 @@ describe API::MavenPackages do
it 'rejects request' do
expect { upload_file_with_token(params) }.not_to change { project.packages.count }
expect(response).to have_gitlab_http_status(400)
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['message']).to include('Validation failed')
end
end
......
......@@ -172,8 +172,8 @@ describe API::NugetPackages do
end
describe 'PUT /api/v4/projects/:id/packages/nuget' do
let_it_be(:workhorse_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') }
let_it_be(:workhorse_header) { { 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => workhorse_token } }
let(:workhorse_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') }
let(:workhorse_header) { { 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => workhorse_token } }
let_it_be(:file_name) { 'package.nupkg' }
let(:url) { "/projects/#{project.id}/packages/nuget" }
let(:headers) { {} }
......
......@@ -72,7 +72,7 @@ RSpec.shared_examples 'process nuget workhorse authorization' do |user_type, sta
project.add_maintainer(user)
end
it_behaves_like 'returning response status', :error
it_behaves_like 'returning response status', :forbidden
end
end
end
......@@ -104,6 +104,7 @@ RSpec.shared_examples 'process nuget upload' do |user_type, status, add_member =
end
context 'with correct params' do
it_behaves_like 'package workhorse uploads'
it_behaves_like 'creates nuget package files'
it_behaves_like 'a gitlab tracking event', described_class.name, 'push_package'
end
......
......@@ -115,3 +115,17 @@ RSpec.shared_examples 'background upload schedules a file migration' do
end
end
end
shared_examples 'package workhorse uploads' do
context 'without a workhorse header' do
let(:workhorse_token) { JWT.encode({ 'iss' => 'invalid header' }, Gitlab::Workhorse.secret, 'HS256') }
it_behaves_like 'returning response status', :forbidden
it 'logs an error' do
expect(Gitlab::ErrorTracking).to receive(:track_exception).once
subject
end
end
end
......@@ -258,11 +258,21 @@ module API
end
def require_gitlab_workhorse!
verify_workhorse_api!
unless env['HTTP_GITLAB_WORKHORSE'].present?
forbidden!('Request should be executed via GitLab Workhorse')
end
end
def verify_workhorse_api!
Gitlab::Workhorse.verify_api_request!(request.headers)
rescue => e
Gitlab::ErrorTracking.track_exception(e)
forbidden!
end
def require_pages_enabled!
not_found! unless user_project.pages_available?
end
......
......@@ -1545,7 +1545,7 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
authorize_artifacts
expect(response).to have_gitlab_http_status(500)
expect(response).to have_gitlab_http_status(:forbidden)
end
context 'authorization token is invalid' do
......@@ -1675,6 +1675,18 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
end
end
context 'Is missing GitLab Workhorse token headers' do
let(:jwt_token) { JWT.encode({ 'iss' => 'invalid-header' }, Gitlab::Workhorse.secret, 'HS256') }
it 'fails to post artifacts without GitLab-Workhorse' do
expect(Gitlab::ErrorTracking).to receive(:track_exception).once
upload_artifacts(file_upload, headers_with_token)
expect(response).to have_gitlab_http_status(:forbidden)
end
end
context 'when setting an expire date' do
let(:default_artifacts_expire_in) {}
let(:post_data) do
......
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