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

Merge branch 'security-path-traversal-npm-package-registry' into 'master'

Validate NPM package versions to be SemVer compliant

Closes #96

See merge request gitlab-org/security/gitlab!359
parents dbf955ce eb1ee151
...@@ -29,6 +29,7 @@ class Packages::Package < ApplicationRecord ...@@ -29,6 +29,7 @@ class Packages::Package < ApplicationRecord
validate :valid_conan_package_recipe, if: :conan? validate :valid_conan_package_recipe, if: :conan?
validate :valid_npm_package_name, if: :npm? validate :valid_npm_package_name, if: :npm?
validate :package_already_taken, if: :npm? validate :package_already_taken, if: :npm?
validates :version, format: { with: Gitlab::Regex.semver_regex }, if: :npm?
enum package_type: { maven: 1, npm: 2, conan: 3, nuget: 4 } enum package_type: { maven: 1, npm: 2, conan: 3, nuget: 4 }
......
---
title: Add NPM package versions SemVer validation
merge_request:
author:
type: security
...@@ -43,6 +43,11 @@ module EE ...@@ -43,6 +43,11 @@ module EE
maven_app_name_regex maven_app_name_regex
end end
def semver_regex
# see the official regex: https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
@semver_regex ||= %r{\A(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?\z}.freeze
end
def feature_flag_regex def feature_flag_regex
/\A[a-z]([-_a-z0-9]*[a-z0-9])?\z/ /\A[a-z]([-_a-z0-9]*[a-z0-9])?\z/
end end
......
...@@ -103,4 +103,17 @@ describe Gitlab::Regex do ...@@ -103,4 +103,17 @@ describe Gitlab::Regex do
it { is_expected.not_to match('my package name') } it { is_expected.not_to match('my package name') }
it { is_expected.not_to match('!!()()') } it { is_expected.not_to match('!!()()') }
end end
describe '.semver_regex' do
subject { described_class.semver_regex }
it { is_expected.to match('1.2.3') }
it { is_expected.to match('1.2.3-beta') }
it { is_expected.to match('1.2.3-alpha.3') }
it { is_expected.not_to match('1') }
it { is_expected.not_to match('1.2') }
it { is_expected.not_to match('1./2.3') }
it { is_expected.not_to match('../../../../../1.2.3') }
it { is_expected.not_to match('%2e%2e%2f1.2.3') }
end
end end
...@@ -80,6 +80,21 @@ RSpec.describe Packages::Package, type: :model do ...@@ -80,6 +80,21 @@ RSpec.describe Packages::Package, type: :model do
it { is_expected.not_to allow_value("my(dom$$$ain)com.my-app").for(:name) } it { is_expected.not_to allow_value("my(dom$$$ain)com.my-app").for(:name) }
end end
describe '#version' do
context 'npm package' do
subject { create(:npm_package) }
it { is_expected.to allow_value('1.2.3').for(:version) }
it { is_expected.to allow_value('1.2.3-beta').for(:version) }
it { is_expected.to allow_value('1.2.3-alpha.3').for(:version) }
it { is_expected.not_to allow_value('1').for(:version) }
it { is_expected.not_to allow_value('1.2').for(:version) }
it { is_expected.not_to allow_value('1./2.3').for(:version) }
it { is_expected.not_to allow_value('../../../../../1.2.3').for(:version) }
it { is_expected.not_to allow_value('%2e%2e%2f1.2.3').for(:version) }
end
end
describe '#package_already_taken' do describe '#package_already_taken' do
context 'npm package' do context 'npm package' do
let!(:package) { create(:npm_package) } let!(:package) { create(:npm_package) }
...@@ -173,7 +188,7 @@ RSpec.describe Packages::Package, type: :model do ...@@ -173,7 +188,7 @@ RSpec.describe Packages::Package, type: :model do
end end
describe '.has_version' do describe '.has_version' do
let!(:package4) { create(:npm_package, version: nil) } let!(:package4) { create(:nuget_package, version: nil) }
subject { described_class.has_version } subject { described_class.has_version }
......
...@@ -246,21 +246,25 @@ describe API::NpmPackages do ...@@ -246,21 +246,25 @@ describe API::NpmPackages do
end end
describe 'PUT /api/v4/projects/:id/packages/npm/:package_name' do describe 'PUT /api/v4/projects/:id/packages/npm/:package_name' do
RSpec.shared_examples 'handling invalid record with 400 error' do
it 'handles an ActiveRecord::RecordInvalid exception with 400 error' do
expect { upload_package_with_token(package_name, params) }
.not_to change { project.packages.count }
expect(response).to have_gitlab_http_status(:bad_request)
end
end
context 'when params are correct' do context 'when params are correct' do
context 'invalid package record' do context 'invalid package record' do
context 'unscoped package' do context 'unscoped package' do
let(:package_name) { 'my_unscoped_package' } let(:package_name) { 'my_unscoped_package' }
let(:params) { upload_params(package_name) } let(:params) { upload_params(package_name: package_name) }
it 'handles an ActiveRecord::RecordInvalid exception with 400 error' do it_behaves_like 'handling invalid record with 400 error'
expect { upload_package_with_token(package_name, params) }
.not_to change { project.packages.count }
expect(response).to have_gitlab_http_status(:bad_request)
end
context 'with empty versions' do context 'with empty versions' do
let(:params) { upload_params(package_name).merge!(versions: {}) } let(:params) { upload_params(package_name: package_name).merge!(versions: {}) }
it 'throws a 400 error' do it 'throws a 400 error' do
expect { upload_package_with_token(package_name, params) } expect { upload_package_with_token(package_name, params) }
...@@ -273,20 +277,37 @@ describe API::NpmPackages do ...@@ -273,20 +277,37 @@ describe API::NpmPackages do
context 'invalid package name' do context 'invalid package name' do
let(:package_name) { "@#{group.path}/my_inv@@lid_package_name" } let(:package_name) { "@#{group.path}/my_inv@@lid_package_name" }
let(:params) { upload_params(package_name) } let(:params) { upload_params(package_name: package_name) }
it 'handles an ActiveRecord::RecordInvalid exception with 400 error' do it_behaves_like 'handling invalid record with 400 error'
expect { upload_package_with_token(package_name, params) } end
.not_to change { project.packages.count }
context 'invalid package version' do
using RSpec::Parameterized::TableSyntax
let(:package_name) { "@#{group.path}/my_package_name" }
where(:version) do
[
'1',
'1.2',
'1./2.3',
'../../../../../1.2.3',
'%2e%2e%2f1.2.3'
]
end
with_them do
let(:params) { upload_params(package_name: package_name, package_version: version) }
expect(response).to have_gitlab_http_status(:bad_request) it_behaves_like 'handling invalid record with 400 error'
end end
end end
end end
context 'scoped package' do context 'scoped package' do
let(:package_name) { "@#{group.path}/my_package_name" } let(:package_name) { "@#{group.path}/my_package_name" }
let(:params) { upload_params(package_name) } let(:params) { upload_params(package_name: package_name) }
context 'with access token' do context 'with access token' do
subject { upload_package_with_token(package_name, params) } subject { upload_package_with_token(package_name, params) }
...@@ -335,7 +356,7 @@ describe API::NpmPackages do ...@@ -335,7 +356,7 @@ describe API::NpmPackages do
context 'package creation fails' do context 'package creation fails' do
let(:package_name) { "@#{group.path}/my_package_name" } let(:package_name) { "@#{group.path}/my_package_name" }
let(:params) { upload_params(package_name) } let(:params) { upload_params(package_name: package_name) }
it 'returns an error if the package already exists' do it 'returns an error if the package already exists' do
create(:npm_package, project: project, version: '1.0.1', name: "@#{group.path}/my_package_name") create(:npm_package, project: project, version: '1.0.1', name: "@#{group.path}/my_package_name")
...@@ -348,7 +369,7 @@ describe API::NpmPackages do ...@@ -348,7 +369,7 @@ describe API::NpmPackages do
context 'with dependencies' do context 'with dependencies' do
let(:package_name) { "@#{group.path}/my_package_name" } let(:package_name) { "@#{group.path}/my_package_name" }
let(:params) { upload_params(package_name, 'npm/payload_with_duplicated_packages.json') } let(:params) { upload_params(package_name: package_name, file: 'npm/payload_with_duplicated_packages.json') }
it 'creates npm package with file and dependencies' do it 'creates npm package with file and dependencies' do
expect { upload_package_with_token(package_name, params) } expect { upload_package_with_token(package_name, params) }
...@@ -363,7 +384,7 @@ describe API::NpmPackages do ...@@ -363,7 +384,7 @@ describe API::NpmPackages do
context 'with existing dependencies' do context 'with existing dependencies' do
before do before do
name = "@#{group.path}/existing_package" name = "@#{group.path}/existing_package"
upload_package_with_token(name, upload_params(name, 'npm/payload_with_duplicated_packages.json')) upload_package_with_token(name, upload_params(package_name: name, file: 'npm/payload_with_duplicated_packages.json'))
end end
it 'reuses them' do it 'reuses them' do
...@@ -389,10 +410,11 @@ describe API::NpmPackages do ...@@ -389,10 +410,11 @@ describe API::NpmPackages do
upload_package(package_name, params.merge(job_token: job.token)) upload_package(package_name, params.merge(job_token: job.token))
end end
def upload_params(package_name, file = 'npm/payload.json') def upload_params(package_name:, package_version: '1.0.1', file: 'npm/payload.json')
JSON.parse( JSON.parse(
fixture_file(file, dir: 'ee') fixture_file(file, dir: 'ee')
.gsub('@root/npm-test', package_name)) .gsub('@root/npm-test', package_name)
.gsub('1.0.1', package_version))
end end
end end
......
...@@ -5,7 +5,7 @@ describe Packages::Npm::CreatePackageService do ...@@ -5,7 +5,7 @@ describe Packages::Npm::CreatePackageService do
let(:namespace) {create(:namespace)} let(:namespace) {create(:namespace)}
let(:project) { create(:project, namespace: namespace) } let(:project) { create(:project, namespace: namespace) }
let(:user) { create(:user) } let(:user) { create(:user) }
let(:version) { '1.0.1'.freeze } let(:version) { '1.0.1' }
let(:params) do let(:params) do
JSON.parse( JSON.parse(
...@@ -37,7 +37,6 @@ describe Packages::Npm::CreatePackageService do ...@@ -37,7 +37,6 @@ describe Packages::Npm::CreatePackageService do
expect(package.version).to eq(version) expect(package.version).to eq(version)
end end
it { is_expected.to be_valid }
it { expect(subject.name).to eq(package_name) } it { expect(subject.name).to eq(package_name) }
it { expect(subject.version).to eq(version) } it { expect(subject.version).to eq(version) }
end end
...@@ -77,5 +76,23 @@ describe Packages::Npm::CreatePackageService do ...@@ -77,5 +76,23 @@ describe Packages::Npm::CreatePackageService do
it { expect(subject[:http_status]).to eq 400 } it { expect(subject[:http_status]).to eq 400 }
it { expect(subject[:message]).to eq 'Version is empty.' } it { expect(subject[:message]).to eq 'Version is empty.' }
end end
context 'with invalid versions' do
using RSpec::Parameterized::TableSyntax
where(:version) do
[
'1',
'1.2',
'1./2.3',
'../../../../../1.2.3',
'%2e%2e%2f1.2.3'
]
end
with_them do
it { expect { subject }.to raise_error(ActiveRecord::RecordInvalid, 'Validation failed: Version is invalid') }
end
end
end end
end end
...@@ -37,7 +37,7 @@ describe Packages::Npm::CreateTagService do ...@@ -37,7 +37,7 @@ describe Packages::Npm::CreateTagService do
end end
context 'on same package with different version' do context 'on same package with different version' do
let!(:package2) { create(:npm_package, project: package.project, name: package.name, version: '5.0.0testing') } let!(:package2) { create(:npm_package, project: package.project, name: package.name, version: '5.0.0-testing') }
it { expect { subject }.to not_change { Packages::Tag.count } } it { expect { subject }.to not_change { Packages::Tag.count } }
it { expect(subject.name).to eq(tag_name) } it { expect(subject.name).to eq(tag_name) }
......
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