Commit 2cfd792a authored by GitLab Release Tools Bot's avatar GitLab Release Tools Bot

Merge remote-tracking branch 'dev/master'

parents 2b83aa8e dd250391
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.
## 13.5.2 (2020-11-02)
### Security (4 changes)
- Sync code owners rules on MR update. !1003
- Fix potential regex backtracking attack in path parsing in search result. !1027
- Transfer missing epics when a project is transferred.
- Tighten the RBAC for GraphQL in SAST CiConfiguration.
## 13.5.1 (2020-10-22) ## 13.5.1 (2020-10-22)
- No changes. - No changes.
...@@ -220,6 +230,16 @@ Please view this file on the master branch, on stable branches it's out of date. ...@@ -220,6 +230,16 @@ Please view this file on the master branch, on stable branches it's out of date.
- Remove bootstrap class in licensed user count. !45443 - Remove bootstrap class in licensed user count. !45443
## 13.4.5 (2020-11-02)
### Security (4 changes)
- Sync code owners rules on MR update. !1003
- Fix potential regex backtracking attack in path parsing in search result. !1023
- Transfer missing epics when a project is transferred.
- Tighten the RBAC for GraphQL in SAST CiConfiguration.
## 13.4.4 (2020-10-15) ## 13.4.4 (2020-10-15)
### Fixed (1 change) ### Fixed (1 change)
...@@ -493,6 +513,16 @@ Please view this file on the master branch, on stable branches it's out of date. ...@@ -493,6 +513,16 @@ Please view this file on the master branch, on stable branches it's out of date.
- Elasticsearch reindexing: add confirmation popup and change color scheme. !42209 - Elasticsearch reindexing: add confirmation popup and change color scheme. !42209
## 13.3.9 (2020-11-02)
### Security (4 changes)
- Sync code owners rules on MR update. !1003
- Fix potential regex backtracking attack in path parsing in search result. !1024
- Transfer missing epics when a project is transferred.
- Tighten the RBAC for GraphQL in SAST CiConfiguration.
## 13.3.8 (2020-10-21) ## 13.3.8 (2020-10-21)
### Fixed (4 changes) ### Fixed (4 changes)
......
...@@ -2,6 +2,21 @@ ...@@ -2,6 +2,21 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. entry.
## 13.5.2 (2020-11-02)
### Security (9 changes)
- Add CSRF protection to runner pause and resume. !1021
- Do not expose Terraform state record in API.
- Path traversal to RCE via LFS upload.
- Update container_repository_name_regex to prevent catastrophic backtracking.
- Validate nuget package names.
- Prevent private repo from being accessed via internal Kubernetes API.
- Validate each upload param key in multipart.rb.
- Fix XSS vulnerability for job build dependencies.
- Fix unauthorized user is able to access schedule pipeline variables and values.
## 13.5.1 (2020-10-22) ## 13.5.1 (2020-10-22)
### Other (1 change) ### Other (1 change)
...@@ -583,6 +598,21 @@ entry. ...@@ -583,6 +598,21 @@ entry.
- Bump cluster applications CI template. !45472 - Bump cluster applications CI template. !45472
## 13.4.5 (2020-11-02)
### Security (9 changes)
- Add CSRF protection to runner pause and resume. !1021
- Do not expose Terraform state record in API.
- Path traversal to RCE via LFS upload.
- Update container_repository_name_regex to prevent catastrophic backtracking.
- Validate nuget package names.
- Prevent private repo from being accessed via internal Kubernetes API.
- Validate each upload param key in multipart.rb.
- Fix XSS vulnerability for job build dependencies.
- Fix unauthorized user is able to access schedule pipeline variables and values.
## 13.4.4 (2020-10-15) ## 13.4.4 (2020-10-15)
### Fixed (2 changes) ### Fixed (2 changes)
...@@ -1241,6 +1271,21 @@ entry. ...@@ -1241,6 +1271,21 @@ entry.
- Expand the visible highlight for collapsed diffs (re: !41393). !42343 - Expand the visible highlight for collapsed diffs (re: !41393). !42343
## 13.3.9 (2020-11-02)
### Security (9 changes)
- Add CSRF protection to runner pause and resume. !1021
- Do not expose Terraform state record in API.
- Path traversal to RCE via LFS upload.
- Update container_repository_name_regex to prevent catastrophic backtracking.
- Validate nuget package names.
- Prevent private repo from being accessed via internal Kubernetes API.
- Validate each upload param key in multipart.rb.
- Fix XSS vulnerability for job build dependencies.
- Fix unauthorized user is able to access schedule pipeline variables and values.
## 13.3.8 (2020-10-21) ## 13.3.8 (2020-10-21)
### Fixed (2 changes) ### Fixed (2 changes)
......
<script> <script>
/* eslint-disable vue/no-v-html */
import { throttle, isEmpty } from 'lodash'; import { throttle, isEmpty } from 'lodash';
import { mapGetters, mapState, mapActions } from 'vuex'; import { mapGetters, mapState, mapActions } from 'vuex';
import { GlLoadingIcon, GlIcon } from '@gitlab/ui'; import { GlLoadingIcon, GlIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils'; import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import { isScrolledToBottom } from '~/lib/utils/scroll_utils'; import { isScrolledToBottom } from '~/lib/utils/scroll_utils';
import { polyfillSticky } from '~/lib/utils/sticky'; import { polyfillSticky } from '~/lib/utils/sticky';
...@@ -36,6 +35,9 @@ export default { ...@@ -36,6 +35,9 @@ export default {
GlLoadingIcon, GlLoadingIcon,
SharedRunner: () => import('ee_component/jobs/components/shared_runner_limit_block.vue'), SharedRunner: () => import('ee_component/jobs/components/shared_runner_limit_block.vue'),
}, },
directives: {
SafeHtml,
},
mixins: [delayedJobMixin], mixins: [delayedJobMixin],
props: { props: {
artifactHelpUrl: { artifactHelpUrl: {
...@@ -223,7 +225,7 @@ export default { ...@@ -223,7 +225,7 @@ export default {
</div> </div>
<callout v-if="shouldRenderHeaderCallout"> <callout v-if="shouldRenderHeaderCallout">
<div v-html="job.callout_message"></div> <div v-safe-html="job.callout_message"></div>
</callout> </callout>
</header> </header>
<!-- EO Header Section --> <!-- EO Header Section -->
......
...@@ -37,6 +37,7 @@ class Packages::Package < ApplicationRecord ...@@ -37,6 +37,7 @@ class Packages::Package < ApplicationRecord
validate :package_already_taken, if: :npm? validate :package_already_taken, if: :npm?
validates :name, format: { with: Gitlab::Regex.conan_recipe_component_regex }, if: :conan? validates :name, format: { with: Gitlab::Regex.conan_recipe_component_regex }, if: :conan?
validates :name, format: { with: Gitlab::Regex.generic_package_name_regex }, if: :generic? validates :name, format: { with: Gitlab::Regex.generic_package_name_regex }, if: :generic?
validates :name, format: { with: Gitlab::Regex.nuget_package_name_regex }, if: :nuget?
validates :version, format: { with: Gitlab::Regex.nuget_version_regex }, if: :nuget? validates :version, format: { with: Gitlab::Regex.nuget_version_regex }, if: :nuget?
validates :version, format: { with: Gitlab::Regex.conan_recipe_component_regex }, if: :conan? validates :version, format: { with: Gitlab::Regex.conan_recipe_component_regex }, if: :conan?
validates :version, format: { with: Gitlab::Regex.maven_version_regex }, if: -> { version? && maven? } validates :version, format: { with: Gitlab::Regex.maven_version_regex }, if: -> { version? && maven? }
......
...@@ -17,6 +17,7 @@ module Ci ...@@ -17,6 +17,7 @@ module Ci
rule { can?(:admin_pipeline) | (can?(:update_build) & owner_of_schedule) }.policy do rule { can?(:admin_pipeline) | (can?(:update_build) & owner_of_schedule) }.policy do
enable :update_pipeline_schedule enable :update_pipeline_schedule
enable :admin_pipeline_schedule enable :admin_pipeline_schedule
enable :read_pipeline_schedule_variables
end end
rule { can?(:admin_pipeline_schedule) & ~owner_of_schedule }.policy do rule { can?(:admin_pipeline_schedule) & ~owner_of_schedule }.policy do
......
...@@ -136,7 +136,7 @@ class BuildDetailsEntity < JobEntity ...@@ -136,7 +136,7 @@ class BuildDetailsEntity < JobEntity
docs_url = "https://docs.gitlab.com/ee/ci/yaml/README.html#dependencies" docs_url = "https://docs.gitlab.com/ee/ci/yaml/README.html#dependencies"
[ [
failure_message.html_safe, failure_message,
help_message(docs_url).html_safe help_message(docs_url).html_safe
].join("<br />") ].join("<br />")
end end
......
...@@ -32,6 +32,8 @@ module Packages ...@@ -32,6 +32,8 @@ module Packages
) )
end end
end end
rescue ActiveRecord::RecordInvalid => e
raise InvalidMetadataError.new(e.message)
end end
private private
......
...@@ -79,11 +79,7 @@ module Projects ...@@ -79,11 +79,7 @@ module Projects
# Directories on disk # Directories on disk
move_project_folders(project) move_project_folders(project)
# Move missing group labels to project transfer_missing_group_resources(@old_group)
Labels::TransferService.new(current_user, @old_group, project).execute
# Move missing group milestones
Milestones::TransferService.new(current_user, @old_group, project).execute
# Move uploads # Move uploads
move_project_uploads(project) move_project_uploads(project)
...@@ -107,6 +103,12 @@ module Projects ...@@ -107,6 +103,12 @@ module Projects
refresh_permissions refresh_permissions
end end
def transfer_missing_group_resources(group)
Labels::TransferService.new(current_user, group, project).execute
Milestones::TransferService.new(current_user, group, project).execute
end
def allowed_transfer?(current_user, project) def allowed_transfer?(current_user, project)
@new_namespace && @new_namespace &&
can?(current_user, :change_namespace, project) && can?(current_user, :change_namespace, project) &&
......
...@@ -23,6 +23,8 @@ module Terraform ...@@ -23,6 +23,8 @@ module Terraform
state.save! unless state.destroyed? state.save! unless state.destroyed?
end end
nil
end end
def lock! def lock!
......
...@@ -5,6 +5,10 @@ class GitlabUploader < CarrierWave::Uploader::Base ...@@ -5,6 +5,10 @@ class GitlabUploader < CarrierWave::Uploader::Base
class_attribute :options class_attribute :options
PROTECTED_METHODS = %i(filename cache_dir work_dir store_dir).freeze
ObjectNotReadyError = Class.new(StandardError)
class << self class << self
# DSL setter # DSL setter
def storage_options(options) def storage_options(options)
...@@ -33,6 +37,8 @@ class GitlabUploader < CarrierWave::Uploader::Base ...@@ -33,6 +37,8 @@ class GitlabUploader < CarrierWave::Uploader::Base
delegate :base_dir, :file_storage?, to: :class delegate :base_dir, :file_storage?, to: :class
before :cache, :protect_from_path_traversal!
def initialize(model, mounted_as = nil, **uploader_context) def initialize(model, mounted_as = nil, **uploader_context)
super(model, mounted_as) super(model, mounted_as)
end end
...@@ -121,6 +127,9 @@ class GitlabUploader < CarrierWave::Uploader::Base ...@@ -121,6 +127,9 @@ class GitlabUploader < CarrierWave::Uploader::Base
# For example, `FileUploader` builds the storage path based on the associated # For example, `FileUploader` builds the storage path based on the associated
# project model's `path_with_namespace` value, which can change when the # project model's `path_with_namespace` value, which can change when the
# project or its containing namespace is moved or renamed. # project or its containing namespace is moved or renamed.
#
# When implementing this method, raise `ObjectNotReadyError` if the model
# does not yet exist, as it will be tested in `#protect_from_path_traversal!`
def dynamic_segment def dynamic_segment
raise(NotImplementedError) raise(NotImplementedError)
end end
...@@ -138,4 +147,21 @@ class GitlabUploader < CarrierWave::Uploader::Base ...@@ -138,4 +147,21 @@ class GitlabUploader < CarrierWave::Uploader::Base
def pathname def pathname
@pathname ||= Pathname.new(path) @pathname ||= Pathname.new(path)
end end
# Protect against path traversal attacks
# This takes a list of methods to test for path traversal, e.g. ../../
# and checks each of them. This uses `.send` so that any potential errors
# don't block the entire set from being tested.
#
# @param [CarrierWave::SanitizedFile]
# @return [Nil]
# @raise [Gitlab::Utils::PathTraversalAttackError]
def protect_from_path_traversal!(file)
PROTECTED_METHODS.each do |method|
Gitlab::Utils.check_path_traversal!(self.send(method)) # rubocop: disable GitlabSecurity/PublicSend
rescue ObjectNotReadyError
# Do nothing. This test was attempted before the file was ready for that method
end
end
end end
...@@ -4,7 +4,6 @@ class JobArtifactUploader < GitlabUploader ...@@ -4,7 +4,6 @@ class JobArtifactUploader < GitlabUploader
extend Workhorse::UploadPath extend Workhorse::UploadPath
include ObjectStorage::Concern include ObjectStorage::Concern
ObjectNotReadyError = Class.new(StandardError)
UnknownFileLocationError = Class.new(StandardError) UnknownFileLocationError = Class.new(StandardError)
storage_options Gitlab.config.artifacts storage_options Gitlab.config.artifacts
...@@ -24,7 +23,9 @@ class JobArtifactUploader < GitlabUploader ...@@ -24,7 +23,9 @@ class JobArtifactUploader < GitlabUploader
private private
def dynamic_segment def dynamic_segment
raise ObjectNotReadyError, 'JobArtifact is not ready' unless model.id # This now tests model.created_at because it can for some reason be nil in the test suite,
# and it's not clear if this is intentional or not
raise ObjectNotReadyError, 'JobArtifact is not ready' unless model.id && model.created_at
if model.hashed_path? if model.hashed_path?
hashed_path hashed_path
......
...@@ -20,6 +20,8 @@ class Packages::PackageFileUploader < GitlabUploader ...@@ -20,6 +20,8 @@ class Packages::PackageFileUploader < GitlabUploader
private private
def dynamic_segment def dynamic_segment
raise ObjectNotReadyError, "Package model not ready" unless model.id
Gitlab::HashedPath.new('packages', model.package.id, 'files', model.id, root_hash: model.package.project_id) Gitlab::HashedPath.new('packages', model.package.id, 'files', model.id, root_hash: model.package.project_id)
end end
end end
...@@ -69,10 +69,10 @@ ...@@ -69,10 +69,10 @@
= sprite_icon('pencil') = sprite_icon('pencil')
.btn-group .btn-group
- if runner.active? - if runner.active?
= link_to [:pause, :admin, runner], method: :get, class: 'gl-button btn btn-default btn-svg has-tooltip', title: _('Pause'), ref: 'tooltip', aria: { label: _('Pause') }, data: { placement: 'top', container: 'body', confirm: _('Are you sure?') } do = link_to [:pause, :admin, runner], method: :post, class: 'gl-button btn btn-default btn-svg has-tooltip', title: _('Pause'), ref: 'tooltip', aria: { label: _('Pause') }, data: { placement: 'top', container: 'body', confirm: _('Are you sure?') } do
= sprite_icon('pause') = sprite_icon('pause')
- else - else
= link_to [:resume, :admin, runner], method: :get, class: 'gl-button btn btn-default btn-svg has-tooltip gl-px-3', title: _('Resume'), ref: 'tooltip', aria: { label: _('Resume') }, data: { placement: 'top', container: 'body'} do = link_to [:resume, :admin, runner], method: :post, class: 'gl-button btn btn-default btn-svg has-tooltip gl-px-3', title: _('Resume'), ref: 'tooltip', aria: { label: _('Resume') }, data: { placement: 'top', container: 'body'} do
= sprite_icon('play') = sprite_icon('play')
.btn-group .btn-group
= link_to [:admin, runner], method: :delete, class: 'gl-button btn btn-danger has-tooltip', title: _('Remove'), ref: 'tooltip', aria: { label: _('Remove') }, data: { placement: 'top', container: 'body', confirm: _('Are you sure?') } do = link_to [:admin, runner], method: :delete, class: 'gl-button btn btn-danger has-tooltip', title: _('Remove'), ref: 'tooltip', aria: { label: _('Remove') }, data: { placement: 'top', container: 'body', confirm: _('Are you sure?') } do
......
...@@ -148,8 +148,8 @@ namespace :admin do ...@@ -148,8 +148,8 @@ namespace :admin do
resources :runners, only: [:index, :show, :update, :destroy] do resources :runners, only: [:index, :show, :update, :destroy] do
member do member do
get :resume post :resume
get :pause post :pause
end end
collection do collection do
......
...@@ -24,6 +24,8 @@ module EE ...@@ -24,6 +24,8 @@ module EE
calls_gitaly: true, calls_gitaly: true,
description: 'SAST CI configuration for the project', description: 'SAST CI configuration for the project',
resolve: -> (project, args, ctx) do resolve: -> (project, args, ctx) do
return unless Ability.allowed?(ctx[:current_user], :download_code, project)
sast_ci_configuration(project) sast_ci_configuration(project)
end end
......
...@@ -42,6 +42,13 @@ module EE ...@@ -42,6 +42,13 @@ module EE
private private
override :after_update
def after_update(merge_request)
super
::MergeRequests::SyncCodeOwnerApprovalRules.new(merge_request).execute
end
override :create_branch_change_note override :create_branch_change_note
def create_branch_change_note(merge_request, branch_type, old_branch, new_branch) def create_branch_change_note(merge_request, branch_type, old_branch, new_branch)
super super
......
...@@ -19,6 +19,13 @@ module EE ...@@ -19,6 +19,13 @@ module EE
old_path_with_namespace: old_path old_path_with_namespace: old_path
).create! ).create!
end end
override :transfer_missing_group_resources
def transfer_missing_group_resources(group)
super
::Epics::TransferService.new(current_user, group, project).execute
end
end end
end end
end end
# frozen_string_literal: true
# Epics::TransferService class
#
# Used for recreating the missing epics when transferring a project to a new group
#
module Epics
class TransferService
attr_reader :current_user, :old_group, :project
def initialize(current_user, old_group, project)
@current_user = current_user
@old_group = old_group
@project = project
end
def execute
return unless old_group.present? && project.group.present?
# If the old group is an ancestor of the new group the epic can remain assigned
return if project.group.ancestors.include?(old_group)
Epic.transaction do
epics_to_transfer.find_each do |epic|
new_epic = create_epic(epic)
update_issues_epic(epic, new_epic)
end
end
end
private
# rubocop: disable CodeReuse/ActiveRecord
def epics_to_transfer
Epic.joins(:issues)
.where(
issues: { project_id: project.id },
group_id: old_group.self_and_descendants
)
end
# rubocop: enable CodeReuse/ActiveRecord
def create_epic(epic)
return unless current_user.can?(:create_epic, project.group)
epic_params = epic.attributes
.slice('title', 'description', 'start_date', 'end_date', 'confidential')
CreateService.new(project.group, current_user, epic_params).execute
end
# rubocop: disable CodeReuse/ActiveRecord
def update_issues_epic(old_epic, new_epic)
issues = old_epic.issues.where(project: project)
issues.each do |issue|
if new_epic.present?
create_epic_issue_link(issue, new_epic)
else
destroy_epic_issue_link(issue, old_epic)
end
end
end
# rubocop: enable CodeReuse/ActiveRecord
def create_epic_issue_link(issue, epic)
link_params = { target_issuable: issue, skip_epic_dates_update: true }
EpicIssues::CreateService.new(epic, current_user, link_params).execute
end
def destroy_epic_issue_link(issue, epic)
link = EpicIssue.find_by_issue_id(issue.id)
EpicIssues::DestroyService.new(link, current_user).execute
end
end
end
...@@ -133,8 +133,7 @@ module Gitlab ...@@ -133,8 +133,7 @@ module Gitlab
def self.parse_search_result(result, project) def self.parse_search_result(result, project)
ref = result["_source"]["blob"]["commit_sha"] ref = result["_source"]["blob"]["commit_sha"]
path = result["_source"]["blob"]["path"] path = result["_source"]["blob"]["path"]
extname = File.extname(path) basename = File.join(File.dirname(path), File.basename(path, '.*'))
basename = path.sub(/#{extname}$/, '')
content = result["_source"]["blob"]["content"] content = result["_source"]["blob"]["content"]
project_id = result['_source']['project_id'].to_i project_id = result['_source']['project_id'].to_i
total_lines = content.lines.size total_lines = content.lines.size
......
...@@ -27,7 +27,7 @@ RSpec.describe GitlabSchema.types['Project'] do ...@@ -27,7 +27,7 @@ RSpec.describe GitlabSchema.types['Project'] do
describe 'sast_ci_configuration' do describe 'sast_ci_configuration' do
include_context 'read ci configuration for sast enabled project' include_context 'read ci configuration for sast enabled project'
let_it_be(:query) do let(:query) do
%( %(
query { query {
project(fullPath: "#{project.full_path}") { project(fullPath: "#{project.full_path}") {
...@@ -110,6 +110,72 @@ RSpec.describe GitlabSchema.types['Project'] do ...@@ -110,6 +110,72 @@ RSpec.describe GitlabSchema.types['Project'] do
expect(analyzer['label']).to eq('Brakeman') expect(analyzer['label']).to eq('Brakeman')
expect(analyzer['enabled']).to eq(true) expect(analyzer['enabled']).to eq(true)
end end
context "with guest user" do
before do
project.add_guest(user)
end
context 'when project is private' do
let(:project) { create(:project, :private, :repository) }
it "returns no configuration" do
secure_analyzers_prefix = subject.dig('data', 'project', 'sastCiConfiguration')
expect(secure_analyzers_prefix).to be_nil
end
end
context 'when project is public' do
let(:project) { create(:project, :public, :repository) }
context 'when repository is accessible by everyone' do
it "returns the project's sast configuration for global variables" do
secure_analyzers_prefix = subject.dig('data', 'project', 'sastCiConfiguration', 'global', 'nodes').first
expect(secure_analyzers_prefix['type']).to eq('string')
expect(secure_analyzers_prefix['field']).to eq('SECURE_ANALYZERS_PREFIX')
end
end
end
end
context "with non-member user" do
before do
project.team.truncate
end
context 'when project is private' do
let(:project) { create(:project, :private, :repository) }
it "returns no configuration" do
secure_analyzers_prefix = subject.dig('data', 'project', 'sastCiConfiguration')
expect(secure_analyzers_prefix).to be_nil
end
end
context 'when project is public' do
let(:project) { create(:project, :public, :repository) }
context 'when repository is accessible by everyone' do
it "returns the project's sast configuration for global variables" do
secure_analyzers_prefix = subject.dig('data', 'project', 'sastCiConfiguration', 'global', 'nodes').first
expect(secure_analyzers_prefix['type']).to eq('string')
expect(secure_analyzers_prefix['field']).to eq('SECURE_ANALYZERS_PREFIX')
end
end
context 'when repository is accessible only by team members' do
it "returns no configuration" do
project.project_feature.update!(merge_requests_access_level: ProjectFeature::DISABLED,
builds_access_level: ProjectFeature::DISABLED,
repository_access_level: ProjectFeature::PRIVATE)
secure_analyzers_prefix = subject.dig('data', 'project', 'sastCiConfiguration')
expect(secure_analyzers_prefix).to be_nil
end
end
end
end
end end
describe 'security_scanners' do describe 'security_scanners' do
......
...@@ -85,12 +85,13 @@ RSpec.describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need ...@@ -85,12 +85,13 @@ RSpec.describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need
describe 'parse_search_result' do describe 'parse_search_result' do
let(:project) { double(:project) } let(:project) { double(:project) }
let(:content) { "foo\nbar\nbaz\n" } let(:content) { "foo\nbar\nbaz\n" }
let(:path) { 'path/file.ext' }
let(:blob) do let(:blob) do
{ {
'blob' => { 'blob' => {
'commit_sha' => 'sha', 'commit_sha' => 'sha',
'content' => content, 'content' => content,
'path' => 'path/file.ext' 'path' => path
} }
} }
end end
...@@ -184,6 +185,21 @@ RSpec.describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need ...@@ -184,6 +185,21 @@ RSpec.describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need
) )
end end
end end
context 'file path in the blob contains potential backtracking regex attack pattern' do
let(:path) { '/group/project/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab.(a+)+$' }
it 'still parses the basename from the path with reasonable amount of time' do
Timeout.timeout(3.seconds) do
parsed = described_class.parse_search_result({ '_source' => blob }, project)
expect(parsed).to be_kind_of(::Gitlab::Search::FoundBlob)
expect(parsed).to have_attributes(
basename: '/group/project/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab'
)
end
end
end
end end
describe 'issues' do describe 'issues' do
......
...@@ -322,5 +322,13 @@ RSpec.describe MergeRequests::UpdateService, :mailer do ...@@ -322,5 +322,13 @@ RSpec.describe MergeRequests::UpdateService, :mailer do
it_behaves_like 'undeletable existing approval rules' it_behaves_like 'undeletable existing approval rules'
end end
end end
it 'updates code owner approval rules' do
expect_next_instance_of(::MergeRequests::SyncCodeOwnerApprovalRules) do |instance|
expect(instance).to receive(:execute)
end
update_merge_request(title: 'Title')
end
end end
end end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Epics::TransferService do
describe '#execute' do
let_it_be(:user) { create(:admin) }
let_it_be(:new_group, refind: true) { create(:group) }
let_it_be(:old_group, refind: true) { create(:group) }
subject(:service) { described_class.new(user, old_group, project) }
context 'when old_group is present' do
let_it_be(:project) { create(:project, namespace: old_group) }
let_it_be(:epic) { create(:epic, group: old_group, title: 'Epic 1')}
let_it_be(:issue_with_epic) { create(:issue, project: project, epic: epic) }
before do
stub_licensed_features(epics: true)
project.add_maintainer(user)
# simulate project transfer
project.update!(group: new_group)
end
context 'when user can create epics in the new group' do
before do
new_group.add_maintainer(user)
end
it 'recreates the missing group epics in the new group' do
expect { service.execute }.to change(project.group.epics, :count).by(1)
new_epic = issue_with_epic.reload.epic
expect(new_epic.group).to eq(new_group)
expect(new_epic.title).to eq(epic.title)
expect(new_epic.description).to eq(epic.description)
expect(new_epic.start_date).to eq(epic.start_date)
expect(new_epic.end_date).to eq(epic.end_date)
expect(new_epic.confidential).to eq(epic.confidential)
end
it 'does not recreate missing epics that are not applied to issues' do
unassigned_epic = create(:epic, group: old_group)
service.execute
new_epics_titles = project.group.reload.epics.pluck(:title)
expect(new_epics_titles).to include(epic.title).and exclude(unassigned_epic.title)
end
context 'when epic is from an descendant group' do
let_it_be(:old_group_subgroup) { create(:group, parent: old_group) }
it 'recreates the missing epic in the new group' do
create(:epic, group: old_group_subgroup)
expect { service.execute }.to change(project.group.epics, :count).by(1)
end
end
context 'when create_epic returns nil' do
before do
allow_next_instance_of(Epics::CreateService) do |instance|
allow(instance).to receive(:execute).and_return(nil)
end
end
it 'removes issues epic' do
service.execute
expect(issue_with_epic.reload.epic).to be_nil
end
end
context 'when assigned epic is confidential' do
before do
[issue_with_epic, epic].each { |issuable| issuable.update!(confidential: true) }
end
it 'creates a new confidential epic in the new group' do
expect { service.execute }.to change(project.group.epics, :count).by(1)
new_epic = issue_with_epic.reload.epic
expect(new_epic).not_to eq(epic.group)
expect(new_epic.title).to eq(epic.title)
expect(new_epic.confidential).to be_truthy
end
end
end
context 'when user is a guest of the new group' do
let_it_be(:guest) { create(:user) }
before do
old_group.add_owner(guest)
project.add_maintainer(user)
new_group.add_guest(guest)
end
it 'does not create a new epic but removes assigned epic' do
service = described_class.new(guest, old_group, project)
expect { service.execute }.not_to change(project.group.epics, :count)
expect(issue_with_epic.reload.epic).to be_nil
end
end
context 'when epics are disabled' do
before do
stub_licensed_features(epics: false)
end
it 'does not create a new epic' do
expect { service.execute }.not_to change(project.group.epics, :count)
end
end
end
context 'when old_group is not present' do
let_it_be(:project) { create(:project, namespace: create(:namespace)) }
let_it_be(:old_group) { nil }
before do
project.update!(namespace: new_group)
end
it 'returns nil' do
expect(described_class.new(user, old_group, project).execute).to be_nil
end
end
context 'when project group is not present' do
let_it_be(:project) { create(:project, group: old_group) }
before do
project.update!(namespace: user.namespace)
end
it 'returns nil' do
expect(described_class.new(user, old_group, project).execute).to be_nil
end
end
end
end
...@@ -53,4 +53,14 @@ RSpec.describe Projects::TransferService do ...@@ -53,4 +53,14 @@ RSpec.describe Projects::TransferService do
end end
end end
end end
context 'missing epics applied to issues' do
it 'delegates transfer to Epics::TransferService' do
expect_next_instance_of(Epics::TransferService, user, project.group, project) do |epics_transfer_service|
expect(epics_transfer_service).to receive(:execute).once.and_call_original
end
subject.execute(group)
end
end
end end
...@@ -38,7 +38,7 @@ module API ...@@ -38,7 +38,7 @@ module API
requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id' requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id'
end end
get ':id/pipeline_schedules/:pipeline_schedule_id' do get ':id/pipeline_schedules/:pipeline_schedule_id' do
present pipeline_schedule, with: Entities::Ci::PipelineScheduleDetails present pipeline_schedule, with: Entities::Ci::PipelineScheduleDetails, user: current_user
end end
desc 'Create a new pipeline schedule' do desc 'Create a new pipeline schedule' do
......
...@@ -5,7 +5,9 @@ module API ...@@ -5,7 +5,9 @@ module API
module Ci module Ci
class PipelineScheduleDetails < PipelineSchedule class PipelineScheduleDetails < PipelineSchedule
expose :last_pipeline, using: ::API::Entities::Ci::PipelineBasic expose :last_pipeline, using: ::API::Entities::Ci::PipelineBasic
expose :variables, using: ::API::Entities::Ci::Variable expose :variables,
using: ::API::Entities::Ci::Variable,
if: ->(schedule, options) { Ability.allowed?(options[:user], :read_pipeline_schedule_variables, schedule) }
end end
end end
end end
......
...@@ -87,7 +87,7 @@ module API ...@@ -87,7 +87,7 @@ module API
# TODO sort out authorization for real # TODO sort out authorization for real
# https://gitlab.com/gitlab-org/gitlab/-/issues/220912 # https://gitlab.com/gitlab-org/gitlab/-/issues/220912
if !project || !project.public? unless Ability.allowed?(nil, :download_code, project)
not_found! not_found!
end end
......
...@@ -41,7 +41,6 @@ module API ...@@ -41,7 +41,6 @@ module API
env['api.format'] = :binary # this bypasses json serialization env['api.format'] = :binary # this bypasses json serialization
body state.latest_file.read body state.latest_file.read
status :ok
end end
end end
...@@ -55,8 +54,10 @@ module API ...@@ -55,8 +54,10 @@ module API
remote_state_handler.handle_with_lock do |state| remote_state_handler.handle_with_lock do |state|
state.update_file!(CarrierWaveStringFile.new(data), version: params[:serial], build: current_authenticated_job) state.update_file!(CarrierWaveStringFile.new(data), version: params[:serial], build: current_authenticated_job)
status :ok
end end
body false
status :ok
end end
desc 'Delete a terraform state of a certain name' desc 'Delete a terraform state of a certain name'
...@@ -66,8 +67,10 @@ module API ...@@ -66,8 +67,10 @@ module API
remote_state_handler.handle_with_lock do |state| remote_state_handler.handle_with_lock do |state|
state.destroy! state.destroy!
status :ok
end end
body false
status :ok
end end
desc 'Lock a terraform state of a certain name' desc 'Lock a terraform state of a certain name'
......
...@@ -31,6 +31,7 @@ module Gitlab ...@@ -31,6 +31,7 @@ module Gitlab
RACK_ENV_KEY = 'HTTP_GITLAB_WORKHORSE_MULTIPART_FIELDS' RACK_ENV_KEY = 'HTTP_GITLAB_WORKHORSE_MULTIPART_FIELDS'
JWT_PARAM_SUFFIX = '.gitlab-workhorse-upload' JWT_PARAM_SUFFIX = '.gitlab-workhorse-upload'
JWT_PARAM_FIXED_KEY = 'upload' JWT_PARAM_FIXED_KEY = 'upload'
REWRITTEN_FIELD_NAME_MAX_LENGTH = 10000.freeze
class Handler class Handler
def initialize(env, message) def initialize(env, message)
...@@ -41,6 +42,8 @@ module Gitlab ...@@ -41,6 +42,8 @@ module Gitlab
def with_open_files def with_open_files
@rewritten_fields.each do |field, tmp_path| @rewritten_fields.each do |field, tmp_path|
raise "invalid field: #{field.inspect}" unless valid_field_name?(field)
parsed_field = Rack::Utils.parse_nested_query(field) parsed_field = Rack::Utils.parse_nested_query(field)
raise "unexpected field: #{field.inspect}" unless parsed_field.count == 1 raise "unexpected field: #{field.inspect}" unless parsed_field.count == 1
...@@ -108,6 +111,17 @@ module Gitlab ...@@ -108,6 +111,17 @@ module Gitlab
private private
def valid_field_name?(name)
# length validation
return false if name.size >= REWRITTEN_FIELD_NAME_MAX_LENGTH
# brackets validation
return false if name.include?('[]') || name.start_with?('[', ']')
return false unless ::Gitlab::Utils.valid_brackets?(name, allow_nested: false)
true
end
def package_allowed_paths def package_allowed_paths
packages_config = ::Gitlab.config.packages packages_config = ::Gitlab.config.packages
return [] unless allow_packages_storage_path?(packages_config) return [] unless allow_packages_storage_path?(packages_config)
...@@ -141,6 +155,8 @@ module Gitlab ...@@ -141,6 +155,8 @@ module Gitlab
class HandlerForJWTParams < Handler class HandlerForJWTParams < Handler
def with_open_files def with_open_files
@rewritten_fields.keys.each do |field| @rewritten_fields.keys.each do |field|
raise "invalid field: #{field.inspect}" unless valid_field_name?(field)
parsed_field = Rack::Utils.parse_nested_query(field) parsed_field = Rack::Utils.parse_nested_query(field)
raise "unexpected field: #{field.inspect}" unless parsed_field.count == 1 raise "unexpected field: #{field.inspect}" unless parsed_field.count == 1
......
...@@ -50,6 +50,10 @@ module Gitlab ...@@ -50,6 +50,10 @@ module Gitlab
maven_app_name_regex maven_app_name_regex
end end
def nuget_package_name_regex
@nuget_package_name_regex ||= %r{\A[-+\.\_a-zA-Z0-9]+\z}.freeze
end
def nuget_version_regex def nuget_version_regex
@nuget_version_regex ||= / @nuget_version_regex ||= /
\A#{_semver_major_minor_patch_regex}(\.\d*)?#{_semver_prerelease_build_regex}\z \A#{_semver_major_minor_patch_regex}(\.\d*)?#{_semver_prerelease_build_regex}\z
...@@ -208,7 +212,7 @@ module Gitlab ...@@ -208,7 +212,7 @@ module Gitlab
# See https://github.com/docker/distribution/blob/master/reference/regexp.go. # See https://github.com/docker/distribution/blob/master/reference/regexp.go.
# #
def container_repository_name_regex def container_repository_name_regex
@container_repository_regex ||= %r{\A[a-z0-9]+((?:[._/]|__|[-]{0,10})[a-z0-9]+)*\Z} @container_repository_regex ||= %r{\A[a-z0-9]+(([._/]|__|-*)[a-z0-9])*\z}
end end
## ##
......
...@@ -10,6 +10,8 @@ module Gitlab ...@@ -10,6 +10,8 @@ module Gitlab
# Also see https://gitlab.com/gitlab-org/gitlab/-/merge_requests/24223#note_284122580 # Also see https://gitlab.com/gitlab-org/gitlab/-/merge_requests/24223#note_284122580
# It also checks for ALT_SEPARATOR aka '\' (forward slash) # It also checks for ALT_SEPARATOR aka '\' (forward slash)
def check_path_traversal!(path) def check_path_traversal!(path)
return unless path.is_a?(String)
path = decode_path(path) path = decode_path(path)
path_regex = /(\A(\.{1,2})\z|\A\.\.[\/\\]|[\/\\]\.\.\z|[\/\\]\.\.[\/\\]|\n)/ path_regex = /(\A(\.{1,2})\z|\A\.\.[\/\\]|[\/\\]\.\.\z|[\/\\]\.\.[\/\\]|\n)/
...@@ -208,5 +210,33 @@ module Gitlab ...@@ -208,5 +210,33 @@ module Gitlab
def stable_sort_by(list) def stable_sort_by(list)
list.sort_by.with_index { |x, idx| [yield(x), idx] } list.sort_by.with_index { |x, idx| [yield(x), idx] }
end end
# Check for valid brackets (`[` and `]`) in a string using this aspects:
# * open brackets count == closed brackets count
# * (optionally) reject nested brackets via `allow_nested: false`
# * open / close brackets coherence, eg. ][[] -> invalid
def valid_brackets?(string = '', allow_nested: true)
# remove everything except brackets
brackets = string.remove(/[^\[\]]/)
return true if brackets.empty?
# balanced counts check
return false if brackets.size.odd?
unless allow_nested
# nested brackets check
return false if brackets.include?('[[') || brackets.include?(']]')
end
# open / close brackets coherence check
untrimmed = brackets
loop do
trimmed = untrimmed.gsub('[]', '')
return true if trimmed.empty?
return false if trimmed == untrimmed
untrimmed = trimmed
end
end
end end
end end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Invalid uploads that must be rejected', :api, :js do
include_context 'file upload requests helpers'
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user, :admin) }
let_it_be(:personal_access_token) { create(:personal_access_token, user: user) }
context 'invalid upload key', :capybara_ignore_server_errors do
let(:api_path) { "/projects/#{project.id}/packages/nuget/" }
let(:url) { capybara_url(api(api_path)) }
let(:file) { fixture_file_upload('spec/fixtures/dk.png') }
subject do
HTTParty.put(
url,
basic_auth: { user: user.username, password: personal_access_token.token },
body: body
)
end
RSpec.shared_examples 'rejecting invalid keys' do |key_name:, message: nil|
context "with invalid key #{key_name}" do
let(:body) { { key_name => file, 'package[test][name]' => 'test' } }
it { expect { subject }.not_to change { Packages::Package.nuget.count } }
it { expect(subject.code).to eq(500) }
it { expect(subject.body).to include(message.presence || "invalid field: \"#{key_name}\"") }
end
end
RSpec.shared_examples 'by rejecting uploads with an invalid key' do
it_behaves_like 'rejecting invalid keys', key_name: 'package[test'
it_behaves_like 'rejecting invalid keys', key_name: '[]'
it_behaves_like 'rejecting invalid keys', key_name: '[package]test'
it_behaves_like 'rejecting invalid keys', key_name: 'package][test]]'
it_behaves_like 'rejecting invalid keys', key_name: 'package[test[nested]]'
end
# These keys are rejected directly by rack itself.
# The request will not be received by multipart.rb (can't use the 'handling file uploads' shared example)
it_behaves_like 'rejecting invalid keys', key_name: 'x' * 11000, message: 'Puma caught this error: exceeded available parameter key space (RangeError)'
it_behaves_like 'rejecting invalid keys', key_name: 'package[]test', message: 'Puma caught this error: expected Hash (got Array)'
it_behaves_like 'handling file uploads', 'by rejecting uploads with an invalid key'
end
end
...@@ -123,15 +123,46 @@ RSpec.describe Gitlab::Middleware::Multipart do ...@@ -123,15 +123,46 @@ RSpec.describe Gitlab::Middleware::Multipart do
end end
end end
context 'with invalid key in parameters' do context 'with an invalid upload key' do
include_context 'with one temporary file for multipart' include_context 'with one temporary file for multipart'
let(:rewritten_fields) { rewritten_fields_hash('file' => uploaded_filepath) } RSpec.shared_examples 'rejecting the invalid key' do |key_in_header:, key_in_upload_params:, error_message:|
let(:params) { upload_parameters_for(filepath: uploaded_filepath, key: 'wrong_key', filename: filename, remote_id: remote_id) } let(:rewritten_fields) { rewritten_fields_hash(key_in_header => uploaded_filepath) }
let(:params) { upload_parameters_for(filepath: uploaded_filepath, key: key_in_upload_params, filename: filename, remote_id: remote_id) }
it 'raises an error' do it 'raises an error' do
expect { subject }.to raise_error(RuntimeError, 'Empty JWT param: file.gitlab-workhorse-upload') expect { subject }.to raise_error(RuntimeError, error_message)
end
end end
it_behaves_like 'rejecting the invalid key',
key_in_header: 'file',
key_in_upload_params: 'wrong_key',
error_message: 'Empty JWT param: file.gitlab-workhorse-upload'
it_behaves_like 'rejecting the invalid key',
key_in_header: 'user[avatar',
key_in_upload_params: 'user[avatar]',
error_message: 'invalid field: "user[avatar"'
it_behaves_like 'rejecting the invalid key',
key_in_header: '[user]avatar',
key_in_upload_params: 'user[avatar]',
error_message: 'invalid field: "[user]avatar"'
it_behaves_like 'rejecting the invalid key',
key_in_header: 'user[]avatar',
key_in_upload_params: 'user[avatar]',
error_message: 'invalid field: "user[]avatar"'
it_behaves_like 'rejecting the invalid key',
key_in_header: 'user[avatar[image[url]]]',
key_in_upload_params: 'user[avatar]',
error_message: 'invalid field: "user[avatar[image[url]]]"'
it_behaves_like 'rejecting the invalid key',
key_in_header: '[]',
key_in_upload_params: 'user[avatar]',
error_message: 'invalid field: "[]"'
it_behaves_like 'rejecting the invalid key',
key_in_header: 'x' * 11000,
key_in_upload_params: 'user[avatar]',
error_message: "invalid field: \"#{'x' * 11000}\""
end end
context 'with a modified JWT payload' do context 'with a modified JWT payload' do
......
...@@ -139,6 +139,58 @@ RSpec.describe Gitlab::Middleware::Multipart do ...@@ -139,6 +139,58 @@ RSpec.describe Gitlab::Middleware::Multipart do
subject subject
end end
end end
context 'with invalid key in header' do
include_context 'with one temporary file for multipart'
RSpec.shared_examples 'rejecting the invalid key' do |key_in_header:, key_in_upload_params:, error_message:|
let(:rewritten_fields) { rewritten_fields_hash(key_in_header => uploaded_filepath) }
let(:params) { upload_parameters_for(filepath: uploaded_filepath, key: key_in_upload_params, filename: filename, remote_id: remote_id) }
it 'raises an error' do
expect { subject }.to raise_error(RuntimeError, error_message)
end
end
it_behaves_like 'rejecting the invalid key',
key_in_header: 'user[avatar',
key_in_upload_params: 'user[avatar]',
error_message: 'invalid field: "user[avatar"'
it_behaves_like 'rejecting the invalid key',
key_in_header: '[user]avatar',
key_in_upload_params: 'user[avatar]',
error_message: 'invalid field: "[user]avatar"'
it_behaves_like 'rejecting the invalid key',
key_in_header: 'user[]avatar',
key_in_upload_params: 'user[avatar]',
error_message: 'invalid field: "user[]avatar"'
it_behaves_like 'rejecting the invalid key',
key_in_header: 'user[avatar[image[url]]]',
key_in_upload_params: 'user[avatar]',
error_message: 'invalid field: "user[avatar[image[url]]]"'
it_behaves_like 'rejecting the invalid key',
key_in_header: '[]',
key_in_upload_params: 'user[avatar]',
error_message: 'invalid field: "[]"'
it_behaves_like 'rejecting the invalid key',
key_in_header: 'x' * 11000,
key_in_upload_params: 'user[avatar]',
error_message: "invalid field: \"#{'x' * 11000}\""
end
context 'with key with unbalanced brackets in header' do
include_context 'with one temporary file for multipart'
let(:invalid_key) { 'user[avatar' }
let(:rewritten_fields) { rewritten_fields_hash( invalid_key => uploaded_filepath) }
let(:params) { upload_parameters_for(filepath: uploaded_filepath, key: 'user[avatar]', filename: filename, remote_id: remote_id) }
it 'builds no UploadedFile' do
expect(app).not_to receive(:call)
expect { subject }.to raise_error(RuntimeError, "invalid field: \"#{invalid_key}\"")
end
end
end end
end end
end end
...@@ -137,11 +137,16 @@ RSpec.describe Gitlab::Regex do ...@@ -137,11 +137,16 @@ RSpec.describe Gitlab::Regex do
it { is_expected.to match('my/awesome/image-1') } it { is_expected.to match('my/awesome/image-1') }
it { is_expected.to match('my/awesome/image.test') } it { is_expected.to match('my/awesome/image.test') }
it { is_expected.to match('my/awesome/image--test') } it { is_expected.to match('my/awesome/image--test') }
# docker distribution allows for infinite `-` it { is_expected.to match('my/image__test') }
# https://github.com/docker/distribution/blob/master/reference/regexp.go#L13 # this example tests for catastrophic backtracking
# but we have a range of 0,10 to add a reasonable limit. it { is_expected.to match('user1/project/a_bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb------------x') }
it { is_expected.not_to match('my/image-----------test') } it { is_expected.not_to match('user1/project/a_bbbbb-------------') }
it { is_expected.not_to match('my/image-.test') } it { is_expected.not_to match('my/image-.test') }
it { is_expected.not_to match('my/image___test') }
it { is_expected.not_to match('my/image_.test') }
it { is_expected.not_to match('my/image_-test') }
it { is_expected.not_to match('my/image..test') }
it { is_expected.not_to match('my/image\ntest') }
it { is_expected.not_to match('.my/image') } it { is_expected.not_to match('.my/image') }
it { is_expected.not_to match('my/image.') } it { is_expected.not_to match('my/image.') }
end end
...@@ -372,6 +377,21 @@ RSpec.describe Gitlab::Regex do ...@@ -372,6 +377,21 @@ RSpec.describe Gitlab::Regex do
it { is_expected.not_to match('%2e%2e%2f1.2.3') } it { is_expected.not_to match('%2e%2e%2f1.2.3') }
end end
describe '.nuget_package_name_regex' do
subject { described_class.nuget_package_name_regex }
it { is_expected.to match('My.Package') }
it { is_expected.to match('My.Package.Mvc') }
it { is_expected.to match('MyPackage') }
it { is_expected.to match('My.23.Package') }
it { is_expected.to match('My23Package') }
it { is_expected.to match('runtime.my-test64.runtime.package.Mvc') }
it { is_expected.to match('my_package') }
it { is_expected.not_to match('My/package') }
it { is_expected.not_to match('../../../my_package') }
it { is_expected.not_to match('%2e%2e%2fmy_package') }
end
describe '.pypi_version_regex' do describe '.pypi_version_regex' do
subject { described_class.pypi_version_regex } subject { described_class.pypi_version_regex }
......
...@@ -3,6 +3,8 @@ ...@@ -3,6 +3,8 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Gitlab::Utils do RSpec.describe Gitlab::Utils do
using RSpec::Parameterized::TableSyntax
delegate :to_boolean, :boolean_to_yes_no, :slugify, :random_string, :which, delegate :to_boolean, :boolean_to_yes_no, :slugify, :random_string, :which,
:ensure_array_from_string, :to_exclusive_sentence, :bytes_to_megabytes, :ensure_array_from_string, :to_exclusive_sentence, :bytes_to_megabytes,
:append_path, :check_path_traversal!, :allowlisted?, :check_allowed_absolute_path!, :decode_path, :ms_to_round_sec, to: :described_class :append_path, :check_path_traversal!, :allowlisted?, :check_allowed_absolute_path!, :decode_path, :ms_to_round_sec, to: :described_class
...@@ -50,6 +52,10 @@ RSpec.describe Gitlab::Utils do ...@@ -50,6 +52,10 @@ RSpec.describe Gitlab::Utils do
expect(check_path_traversal!('dir/..foo.rb')).to eq('dir/..foo.rb') expect(check_path_traversal!('dir/..foo.rb')).to eq('dir/..foo.rb')
expect(check_path_traversal!('dir/.foo.rb')).to eq('dir/.foo.rb') expect(check_path_traversal!('dir/.foo.rb')).to eq('dir/.foo.rb')
end end
it 'does nothing for a non-string' do
expect(check_path_traversal!(nil)).to be_nil
end
end end
describe '.allowlisted?' do describe '.allowlisted?' do
...@@ -448,4 +454,29 @@ RSpec.describe Gitlab::Utils do ...@@ -448,4 +454,29 @@ RSpec.describe Gitlab::Utils do
end end
end end
end end
describe '.valid_brackets?' do
where(:input, :allow_nested, :valid) do
'no brackets' | true | true
'no brackets' | false | true
'user[avatar]' | true | true
'user[avatar]' | false | true
'user[avatar][friends]' | true | true
'user[avatar][friends]' | false | true
'user[avatar[image[url]]]' | true | true
'user[avatar[image[url]]]' | false | false
'user[avatar[]friends]' | true | true
'user[avatar[]friends]' | false | false
'user[avatar]]' | true | false
'user[avatar]]' | false | false
'user][avatar]]' | true | false
'user][avatar]]' | false | false
'user[avatar' | true | false
'user[avatar' | false | false
end
with_them do
it { expect(described_class.valid_brackets?(input, allow_nested: allow_nested)).to eq(valid) }
end
end
end end
...@@ -122,6 +122,21 @@ RSpec.describe Packages::Package, type: :model do ...@@ -122,6 +122,21 @@ RSpec.describe Packages::Package, type: :model do
it { is_expected.not_to allow_value('my file name').for(:name) } it { is_expected.not_to allow_value('my file name').for(:name) }
it { is_expected.not_to allow_value('!!().for(:name)().for(:name)').for(:name) } it { is_expected.not_to allow_value('!!().for(:name)().for(:name)').for(:name) }
end end
context 'nuget package' do
subject { build_stubbed(:nuget_package) }
it { is_expected.to allow_value('My.Package').for(:name) }
it { is_expected.to allow_value('My.Package.Mvc').for(:name) }
it { is_expected.to allow_value('MyPackage').for(:name) }
it { is_expected.to allow_value('My.23.Package').for(:name) }
it { is_expected.to allow_value('My23Package').for(:name) }
it { is_expected.to allow_value('runtime.my-test64.runtime.package.Mvc').for(:name) }
it { is_expected.to allow_value('my_package').for(:name) }
it { is_expected.not_to allow_value('My/package').for(:name) }
it { is_expected.not_to allow_value('../../../my_package').for(:name) }
it { is_expected.not_to allow_value('%2e%2e%2fmy_package').for(:name) }
end
end end
describe '#version' do describe '#version' do
......
...@@ -137,7 +137,7 @@ RSpec.describe ProjectPolicy do ...@@ -137,7 +137,7 @@ RSpec.describe ProjectPolicy do
it 'disallows all permissions except pipeline when the feature is disabled' do it 'disallows all permissions except pipeline when the feature is disabled' do
builds_permissions = [ builds_permissions = [
:create_build, :read_build, :update_build, :admin_build, :destroy_build, :create_build, :read_build, :update_build, :admin_build, :destroy_build,
:create_pipeline_schedule, :read_pipeline_schedule, :update_pipeline_schedule, :admin_pipeline_schedule, :destroy_pipeline_schedule, :create_pipeline_schedule, :read_pipeline_schedule_variables, :update_pipeline_schedule, :admin_pipeline_schedule, :destroy_pipeline_schedule,
:create_environment, :read_environment, :update_environment, :admin_environment, :destroy_environment, :create_environment, :read_environment, :update_environment, :admin_environment, :destroy_environment,
:create_cluster, :read_cluster, :update_cluster, :admin_cluster, :destroy_cluster, :create_cluster, :read_cluster, :update_cluster, :admin_cluster, :destroy_cluster,
:create_deployment, :read_deployment, :update_deployment, :admin_deployment, :destroy_deployment :create_deployment, :read_deployment, :update_deployment, :admin_deployment, :destroy_deployment
......
...@@ -97,46 +97,112 @@ RSpec.describe API::Ci::PipelineSchedules do ...@@ -97,46 +97,112 @@ RSpec.describe API::Ci::PipelineSchedules do
pipeline_schedule.pipelines << build(:ci_pipeline, project: project) pipeline_schedule.pipelines << build(:ci_pipeline, project: project)
end end
context 'authenticated user with valid permissions' do matcher :return_pipeline_schedule_sucessfully do
it 'returns pipeline_schedule details' do match_unless_raises do |reponse|
get api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}", developer)
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('pipeline_schedule') expect(response).to match_response_schema('pipeline_schedule')
end end
end
it 'responds with 404 Not Found if requesting non-existing pipeline_schedule' do shared_context 'request with project permissions' do
get api("/projects/#{project.id}/pipeline_schedules/-5", developer) context 'authenticated user with project permisions' do
before do
project.add_maintainer(user)
end
expect(response).to have_gitlab_http_status(:not_found) it 'returns pipeline_schedule details' do
get api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}", user)
expect(response).to return_pipeline_schedule_sucessfully
expect(json_response).to have_key('variables')
end
end end
end end
context 'authenticated user with invalid permissions' do shared_examples 'request with schedule ownership' do
it 'does not return pipeline_schedules list' do context 'authenticated user with pipeline schedule ownership' do
get api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}", user) it 'returns pipeline_schedule details' do
get api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}", developer)
expect(response).to have_gitlab_http_status(:not_found) expect(response).to return_pipeline_schedule_sucessfully
expect(json_response).to have_key('variables')
end
end end
end end
context 'authenticated user with insufficient permissions' do shared_examples 'request with unauthenticated user' do
before do context 'with unauthenticated user' do
project.add_guest(user) it 'does not return pipeline_schedule' do
get api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}")
expect(response).to have_gitlab_http_status(:unauthorized)
end
end end
end
it 'does not return pipeline_schedules list' do shared_examples 'request with non-existing pipeline_schedule' do
get api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}", user) it 'responds with 404 Not Found if requesting non-existing pipeline_schedule' do
get api("/projects/#{project.id}/pipeline_schedules/-5", developer)
expect(response).to have_gitlab_http_status(:not_found) expect(response).to have_gitlab_http_status(:not_found)
end end
end end
context 'unauthenticated user' do context 'with private project' do
it 'does not return pipeline_schedules list' do it_behaves_like 'request with schedule ownership'
get api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}") it_behaves_like 'request with project permissions'
it_behaves_like 'request with unauthenticated user'
it_behaves_like 'request with non-existing pipeline_schedule'
expect(response).to have_gitlab_http_status(:unauthorized) context 'authenticated user with no project permissions' do
it 'does not return pipeline_schedule' do
get api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}", user)
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'authenticated user with insufficient project permissions' do
before do
project.add_guest(user)
end
it 'does not return pipeline_schedule' do
get api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}", user)
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
context 'with public project' do
let_it_be(:project) { create(:project, :repository, :public, public_builds: false) }
it_behaves_like 'request with schedule ownership'
it_behaves_like 'request with project permissions'
it_behaves_like 'request with unauthenticated user'
it_behaves_like 'request with non-existing pipeline_schedule'
context 'authenticated user with no project permissions' do
it 'returns pipeline_schedule with no variables' do
get api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}", user)
expect(response).to return_pipeline_schedule_sucessfully
expect(json_response).not_to have_key('variables')
end
end
context 'authenticated user with insufficient project permissions' do
before do
project.add_guest(user)
end
it 'returns pipeline_schedule with no variables' do
get api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}", user)
expect(response).to return_pipeline_schedule_sucessfully
expect(json_response).not_to have_key('variables')
end
end end
end end
end end
......
...@@ -166,6 +166,16 @@ RSpec.describe API::Internal::Kubernetes do ...@@ -166,6 +166,16 @@ RSpec.describe API::Internal::Kubernetes do
) )
) )
end end
context 'repository is for project members only' do
let(:project) { create(:project, :public, :repository_private) }
it 'returns 404' do
send_request(params: { id: project.id }, headers: { 'Authorization' => "Bearer #{agent_token.token}" })
expect(response).to have_gitlab_http_status(:not_found)
end
end
end end
context 'project is private' do context 'project is private' do
...@@ -190,7 +200,7 @@ RSpec.describe API::Internal::Kubernetes do ...@@ -190,7 +200,7 @@ RSpec.describe API::Internal::Kubernetes do
context 'project does not exist' do context 'project does not exist' do
it 'returns 404' do it 'returns 404' do
send_request(params: { id: 0 }, headers: { 'Authorization' => "Bearer #{agent_token.token}" }) send_request(params: { id: non_existing_record_id }, headers: { 'Authorization' => "Bearer #{agent_token.token}" })
expect(response).to have_gitlab_http_status(:not_found) expect(response).to have_gitlab_http_status(:not_found)
end end
......
...@@ -125,6 +125,7 @@ RSpec.describe API::Terraform::State do ...@@ -125,6 +125,7 @@ RSpec.describe API::Terraform::State do
expect { request }.to change { Terraform::State.count }.by(0) expect { request }.to change { Terraform::State.count }.by(0)
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
expect(Gitlab::Json.parse(response.body)).to be_empty
end end
context 'on Unicorn', :unicorn do context 'on Unicorn', :unicorn do
...@@ -132,6 +133,7 @@ RSpec.describe API::Terraform::State do ...@@ -132,6 +133,7 @@ RSpec.describe API::Terraform::State do
expect { request }.to change { Terraform::State.count }.by(0) expect { request }.to change { Terraform::State.count }.by(0)
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
expect(Gitlab::Json.parse(response.body)).to be_empty
end end
end end
end end
...@@ -167,6 +169,7 @@ RSpec.describe API::Terraform::State do ...@@ -167,6 +169,7 @@ RSpec.describe API::Terraform::State do
expect { request }.to change { Terraform::State.count }.by(1) expect { request }.to change { Terraform::State.count }.by(1)
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
expect(Gitlab::Json.parse(response.body)).to be_empty
end end
context 'on Unicorn', :unicorn do context 'on Unicorn', :unicorn do
...@@ -174,6 +177,7 @@ RSpec.describe API::Terraform::State do ...@@ -174,6 +177,7 @@ RSpec.describe API::Terraform::State do
expect { request }.to change { Terraform::State.count }.by(1) expect { request }.to change { Terraform::State.count }.by(1)
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
expect(Gitlab::Json.parse(response.body)).to be_empty
end end
end end
end end
...@@ -218,10 +222,11 @@ RSpec.describe API::Terraform::State do ...@@ -218,10 +222,11 @@ RSpec.describe API::Terraform::State do
context 'with maintainer permissions' do context 'with maintainer permissions' do
let(:current_user) { maintainer } let(:current_user) { maintainer }
it 'deletes the state' do it 'deletes the state and returns empty body' do
expect { request }.to change { Terraform::State.count }.by(-1) expect { request }.to change { Terraform::State.count }.by(-1)
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
expect(Gitlab::Json.parse(response.body)).to be_empty
end end
end end
......
...@@ -198,24 +198,26 @@ RSpec.describe Packages::Nuget::UpdatePackageFromMetadataService, :clean_gitlab_ ...@@ -198,24 +198,26 @@ RSpec.describe Packages::Nuget::UpdatePackageFromMetadataService, :clean_gitlab_
it_behaves_like 'raising an', ::Packages::Nuget::MetadataExtractionService::ExtractionError it_behaves_like 'raising an', ::Packages::Nuget::MetadataExtractionService::ExtractionError
end end
context 'with package file with a blank package name' do context 'with an invalid package name' do
before do invalid_names = [
allow(service).to receive(:package_name).and_return('') '',
end 'My/package',
'../../../my_package',
'%2e%2e%2fmy_package'
]
it_behaves_like 'raising an', ::Packages::Nuget::UpdatePackageFromMetadataService::InvalidMetadataError invalid_names.each do |invalid_name|
end before do
allow(service).to receive(:package_name).and_return(invalid_name)
end
context 'with package file with a blank package version' do it_behaves_like 'raising an', ::Packages::Nuget::UpdatePackageFromMetadataService::InvalidMetadataError
before do
allow(service).to receive(:package_version).and_return('')
end end
it_behaves_like 'raising an', ::Packages::Nuget::UpdatePackageFromMetadataService::InvalidMetadataError
end end
context 'with an invalid package version' do context 'with an invalid package version' do
invalid_versions = [ invalid_versions = [
'',
'555', '555',
'1.2', '1.2',
'1./2.3', '1./2.3',
...@@ -224,13 +226,11 @@ RSpec.describe Packages::Nuget::UpdatePackageFromMetadataService, :clean_gitlab_ ...@@ -224,13 +226,11 @@ RSpec.describe Packages::Nuget::UpdatePackageFromMetadataService, :clean_gitlab_
] ]
invalid_versions.each do |invalid_version| invalid_versions.each do |invalid_version|
it "raises an error for version #{invalid_version}" do before do
allow(service).to receive(:package_version).and_return(invalid_version) allow(service).to receive(:package_version).and_return(invalid_version)
expect { subject }.to raise_error(ActiveRecord::RecordInvalid, 'Validation failed: Version is invalid')
expect(package_file.file_name).not_to include(invalid_version)
expect(package_file.file.file.path).not_to include(invalid_version)
end end
it_behaves_like 'raising an', ::Packages::Nuget::UpdatePackageFromMetadataService::InvalidMetadataError
end end
end end
end end
......
...@@ -42,17 +42,17 @@ RSpec.describe Terraform::RemoteStateHandler do ...@@ -42,17 +42,17 @@ RSpec.describe Terraform::RemoteStateHandler do
describe '#handle_with_lock' do describe '#handle_with_lock' do
it 'allows to modify a state using database locking' do it 'allows to modify a state using database locking' do
state = subject.handle_with_lock do |state| record = nil
subject.handle_with_lock do |state|
record = state
state.name = 'updated-name' state.name = 'updated-name'
end end
expect(state.name).to eq 'updated-name' expect(record.reload.name).to eq 'updated-name'
end end
it 'returns the state object itself' do it 'returns nil' do
state = subject.handle_with_lock expect(subject.handle_with_lock).to be_nil
expect(state.name).to eq 'my-state'
end end
end end
...@@ -70,11 +70,13 @@ RSpec.describe Terraform::RemoteStateHandler do ...@@ -70,11 +70,13 @@ RSpec.describe Terraform::RemoteStateHandler do
it 'handles a locked state using exclusive read lock' do it 'handles a locked state using exclusive read lock' do
handler.lock! handler.lock!
state = handler.handle_with_lock do |state| record = nil
handler.handle_with_lock do |state|
record = state
state.name = 'new-name' state.name = 'new-name'
end end
expect(state.name).to eq 'new-name' expect(record.reload.name).to eq 'new-name'
end end
it 'raises exception if lock has not been acquired before' do it 'raises exception if lock has not been acquired before' do
......
...@@ -14,6 +14,7 @@ end ...@@ -14,6 +14,7 @@ end
RSpec.shared_examples "builds correct paths" do |**patterns| RSpec.shared_examples "builds correct paths" do |**patterns|
let(:patterns) { patterns } let(:patterns) { patterns }
let(:fixture) { File.join('spec', 'fixtures', 'rails_sample.jpg') }
before do before do
allow(subject).to receive(:filename).and_return('<filename>') allow(subject).to receive(:filename).and_return('<filename>')
...@@ -55,4 +56,15 @@ RSpec.shared_examples "builds correct paths" do |**patterns| ...@@ -55,4 +56,15 @@ RSpec.shared_examples "builds correct paths" do |**patterns|
let(:target) { subject.class } let(:target) { subject.class }
end end
end end
describe "path traversal exploits" do
before do
allow(subject).to receive(:filename).and_return("3bc58d54542d6a5efffa9a87554faac0254f73f675b337899ea869f6d38b7371/122../../../../../../../../.ssh/authorized_keys")
end
it "throws an exception" do
expect { subject.cache!(fixture_file_upload(fixture)) }.to raise_error(Gitlab::Utils::PathTraversalAttackError)
expect { subject.store!(fixture_file_upload(fixture)) }.to raise_error(Gitlab::Utils::PathTraversalAttackError)
end
end
end end
...@@ -24,9 +24,14 @@ RSpec.describe ImportExportUploader do ...@@ -24,9 +24,14 @@ RSpec.describe ImportExportUploader do
include_context 'with storage', described_class::Store::REMOTE include_context 'with storage', described_class::Store::REMOTE
it_behaves_like 'builds correct paths', patterns = {
store_dir: %r[import_export_upload/import_file/], store_dir: %r[import_export_upload/import_file/],
upload_path: %r[import_export_upload/import_file/] upload_path: %r[import_export_upload/import_file/]
}
it_behaves_like 'builds correct paths', patterns do
let(:fixture) { File.join('spec', 'fixtures', 'group_export.tar.gz') }
end
describe '#move_to_store' do describe '#move_to_store' do
it 'returns false' do it 'returns false' do
......
...@@ -13,6 +13,18 @@ RSpec.describe Packages::Nuget::ExtractionWorker, type: :worker do ...@@ -13,6 +13,18 @@ RSpec.describe Packages::Nuget::ExtractionWorker, type: :worker do
subject { described_class.new.perform(package_file_id) } subject { described_class.new.perform(package_file_id) }
shared_examples 'handling the metadata error' do |exception_class: ::Packages::Nuget::UpdatePackageFromMetadataService::InvalidMetadataError|
it 'removes the package and the package file' do
expect(Gitlab::ErrorTracking).to receive(:log_exception).with(
instance_of(exception_class),
project_id: package.project_id
)
expect { subject }
.to change { Packages::Package.count }.by(-1)
.and change { Packages::PackageFile.count }.by(-1)
end
end
context 'with valid package file' do context 'with valid package file' do
it 'updates package and package file' do it 'updates package and package file' do
expect { subject } expect { subject }
...@@ -48,46 +60,46 @@ RSpec.describe Packages::Nuget::ExtractionWorker, type: :worker do ...@@ -48,46 +60,46 @@ RSpec.describe Packages::Nuget::ExtractionWorker, type: :worker do
allow_any_instance_of(Zip::File).to receive(:glob).and_return([]) allow_any_instance_of(Zip::File).to receive(:glob).and_return([])
end end
it 'removes the package and the package file' do it_behaves_like 'handling the metadata error', exception_class: ::Packages::Nuget::MetadataExtractionService::ExtractionError
expect(Gitlab::ErrorTracking).to receive(:log_exception).with(
instance_of(::Packages::Nuget::MetadataExtractionService::ExtractionError),
project_id: package.project_id
)
expect { subject }
.to change { Packages::Package.count }.by(-1)
.and change { Packages::PackageFile.count }.by(-1)
end
end end
context 'with package file with a blank package name' do context 'with package with an invalid package name' do
before do invalid_names = [
allow_any_instance_of(::Packages::Nuget::UpdatePackageFromMetadataService).to receive(:package_name).and_return('') '',
end 'My/package',
'../../../my_package',
'%2e%2e%2fmy_package'
]
it 'removes the package and the package file' do invalid_names.each do |invalid_name|
expect(Gitlab::ErrorTracking).to receive(:log_exception).with( before do
instance_of(::Packages::Nuget::UpdatePackageFromMetadataService::InvalidMetadataError), allow_next_instance_of(::Packages::Nuget::UpdatePackageFromMetadataService) do |service|
project_id: package.project_id allow(service).to receive(:package_name).and_return(invalid_name)
) end
expect { subject } end
.to change { Packages::Package.count }.by(-1)
.and change { Packages::PackageFile.count }.by(-1) it_behaves_like 'handling the metadata error'
end end
end end
context 'with package file with a blank package version' do context 'with package with an invalid package version' do
before do invalid_versions = [
allow_any_instance_of(::Packages::Nuget::UpdatePackageFromMetadataService).to receive(:package_version).and_return('') '',
end '555',
'1.2',
'1./2.3',
'../../../../../1.2.3',
'%2e%2e%2f1.2.3'
]
it 'removes the package and the package file' do invalid_versions.each do |invalid_version|
expect(Gitlab::ErrorTracking).to receive(:log_exception).with( before do
instance_of(::Packages::Nuget::UpdatePackageFromMetadataService::InvalidMetadataError), allow_next_instance_of(::Packages::Nuget::UpdatePackageFromMetadataService) do |service|
project_id: package.project_id allow(service).to receive(:package_version).and_return(invalid_version)
) end
expect { subject } end
.to change { Packages::Package.count }.by(-1)
.and change { Packages::PackageFile.count }.by(-1) it_behaves_like 'handling the metadata error'
end end
end end
end end
......
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