Commit 9ab1ac4f authored by John Jarvis's avatar John Jarvis

Merge branch 'security-11-5' of dev.gitlab.org:gitlab/gitlabhq into 11-5-stable

parents 90f56e81 0cd50746
......@@ -244,7 +244,7 @@ class GfmAutoComplete {
displayTpl(value) {
let tmpl = GfmAutoComplete.Loading.template;
if (value.title != null) {
tmpl = GfmAutoComplete.Milestones.template;
tmpl = GfmAutoComplete.Milestones.templateFunction(value.title);
}
return tmpl;
},
......@@ -311,7 +311,7 @@ class GfmAutoComplete {
searchKey: 'search',
data: GfmAutoComplete.defaultLoadingData,
displayTpl(value) {
let tmpl = GfmAutoComplete.Labels.template;
let tmpl = GfmAutoComplete.Labels.templateFunction(value.color, value.title);
if (GfmAutoComplete.isLoading(value)) {
tmpl = GfmAutoComplete.Loading.template;
}
......@@ -576,9 +576,11 @@ GfmAutoComplete.Members = {
},
};
GfmAutoComplete.Labels = {
template:
// eslint-disable-next-line no-template-curly-in-string
'<li><span class="dropdown-label-box" style="background: ${color}"></span> ${title}</li>',
templateFunction(color, title) {
return `<li><span class="dropdown-label-box" style="background: ${_.escape(
color,
)}"></span> ${_.escape(title)}</li>`;
},
};
// Issues, MergeRequests and Snippets
GfmAutoComplete.Issues = {
......@@ -588,8 +590,9 @@ GfmAutoComplete.Issues = {
};
// Milestones
GfmAutoComplete.Milestones = {
// eslint-disable-next-line no-template-curly-in-string
template: '<li>${title}</li>',
templateFunction(title) {
return `<li>${_.escape(title)}</li>`;
},
};
GfmAutoComplete.Loading = {
template:
......
......@@ -4,7 +4,7 @@ module Groups
module Settings
class CiCdController < Groups::ApplicationController
skip_cross_project_access_check :show
before_action :authorize_admin_pipeline!
before_action :authorize_admin_group!
def show
define_ci_variables
......@@ -26,8 +26,8 @@ module Groups
.map { |variable| variable.present(current_user: current_user) }
end
def authorize_admin_pipeline!
return render_404 unless can?(current_user, :admin_pipeline, group)
def authorize_admin_group!
return render_404 unless can?(current_user, :admin_group, group)
end
end
end
......
......@@ -19,6 +19,7 @@ class ProjectsController < Projects::ApplicationController
before_action :lfs_blob_ids, only: [:show], if: [:repo_exists?, :project_view_files?]
before_action :project_export_enabled, only: [:export, :download_export, :remove_export, :generate_new_export]
before_action :present_project, only: [:edit]
before_action :authorize_download_code!, only: [:refs]
# Authorize
before_action :authorize_admin_project!, only: [:edit, :update, :housekeeping, :download_export, :export, :remove_export, :generate_new_export]
......
......@@ -10,6 +10,7 @@ module Ci
include Importable
include Gitlab::Utils::StrongMemoize
include Deployable
include HasRef
belongs_to :project, inverse_of: :builds
belongs_to :runner
......@@ -152,6 +153,10 @@ module Ci
.execute(build)
# rubocop: enable CodeReuse/ServiceClass
end
def find_running_by_token(token)
running.find_by_token(token)
end
end
state_machine :status do
......@@ -638,11 +643,11 @@ module Ci
def secret_group_variables
return [] unless project.group
project.group.ci_variables_for(ref, project)
project.group.ci_variables_for(git_ref, project)
end
def secret_project_variables(environment: persisted_environment)
project.ci_variables_for(ref: ref, environment: environment)
project.ci_variables_for(ref: git_ref, environment: environment)
end
def steps
......
......@@ -11,6 +11,7 @@ module Ci
include Gitlab::Utils::StrongMemoize
include AtomicInternalId
include EnumWithNil
include HasRef
belongs_to :project, inverse_of: :pipelines
belongs_to :user
......@@ -374,10 +375,6 @@ module Ci
@commit ||= Commit.lazy(project, sha)
end
def branch?
!tag?
end
def stuck?
pending_builds.any?(&:stuck?)
end
......@@ -577,7 +574,7 @@ module Ci
end
def protected_ref?
strong_memoize(:protected_ref) { project.protected_for?(ref) }
strong_memoize(:protected_ref) { project.protected_for?(git_ref) }
end
def legacy_trigger
......@@ -697,16 +694,6 @@ module Ci
end
end
def git_ref
if branch?
Gitlab::Git::BRANCH_REF_PREFIX + ref.to_s
elsif tag?
Gitlab::Git::TAG_REF_PREFIX + ref.to_s
else
raise ArgumentError, 'Invalid pipeline type!'
end
end
def latest_builds_status
return 'failed' unless yaml_errors.blank?
......
# frozen_string_literal: true
module HasRef
extend ActiveSupport::Concern
def branch?
!tag?
end
def git_ref
if branch?
Gitlab::Git::BRANCH_REF_PREFIX + ref.to_s
elsif tag?
Gitlab::Git::TAG_REF_PREFIX + ref.to_s
end
end
end
......@@ -300,10 +300,9 @@ class Project < ActiveRecord::Base
validates :namespace, presence: true
validates :name, uniqueness: { scope: :namespace_id }
validates :import_url, url: { protocols: ->(project) { project.persisted? ? VALID_MIRROR_PROTOCOLS : VALID_IMPORT_PROTOCOLS },
ports: ->(project) { project.persisted? ? VALID_MIRROR_PORTS : VALID_IMPORT_PORTS },
allow_localhost: false,
enforce_user: true }, if: [:external_import?, :import_url_changed?]
validates :import_url, public_url: { protocols: ->(project) { project.persisted? ? VALID_MIRROR_PROTOCOLS : VALID_IMPORT_PROTOCOLS },
ports: ->(project) { project.persisted? ? VALID_MIRROR_PORTS : VALID_IMPORT_PORTS },
enforce_user: true }, if: [:external_import?, :import_url_changed?]
validates :star_count, numericality: { greater_than_or_equal_to: 0 }
validate :check_limit, on: :create
validate :check_repository_path_availability, on: :update, if: ->(project) { project.renamed? }
......@@ -1818,10 +1817,21 @@ class Project < ActiveRecord::Base
end
def protected_for?(ref)
if repository.branch_exists?(ref)
ProtectedBranch.protected?(self, ref)
elsif repository.tag_exists?(ref)
ProtectedTag.protected?(self, ref)
raise Repository::AmbiguousRefError if repository.ambiguous_ref?(ref)
resolved_ref = repository.expand_ref(ref) || ref
return false unless Gitlab::Git.tag_ref?(resolved_ref) || Gitlab::Git.branch_ref?(resolved_ref)
ref_name = if resolved_ref == ref
Gitlab::Git.ref_name(resolved_ref)
else
ref
end
if Gitlab::Git.branch_ref?(resolved_ref)
ProtectedBranch.protected?(self, ref_name)
elsif Gitlab::Git.tag_ref?(resolved_ref)
ProtectedTag.protected?(self, ref_name)
end
end
......
......@@ -18,7 +18,7 @@ class RemoteMirror < ActiveRecord::Base
belongs_to :project, inverse_of: :remote_mirrors
validates :url, presence: true, url: { protocols: %w(ssh git http https), allow_blank: true, enforce_user: true }
validates :url, presence: true, public_url: { protocols: %w(ssh git http https), allow_blank: true, enforce_user: true }
before_save :set_new_remote_name, if: :mirror_url_changed?
......
......@@ -26,6 +26,7 @@ class Repository
delegate :bundle_to_disk, to: :raw_repository
CreateTreeError = Class.new(StandardError)
AmbiguousRefError = Class.new(StandardError)
# Methods that cache data from the Git repository.
#
......@@ -176,6 +177,18 @@ class Repository
tags.find { |tag| tag.name == name }
end
def ambiguous_ref?(ref)
tag_exists?(ref) && branch_exists?(ref)
end
def expand_ref(ref)
if tag_exists?(ref)
Gitlab::Git::TAG_REF_PREFIX + ref
elsif branch_exists?(ref)
Gitlab::Git::BRANCH_REF_PREFIX + ref
end
end
def add_branch(user, branch_name, ref)
branch = raw_repository.add_branch(branch_name, user: user, target: ref)
......
......@@ -4,6 +4,11 @@ class Todo < ActiveRecord::Base
include Sortable
include FromUnion
# Time to wait for todos being removed when not visible for user anymore.
# Prevents TODOs being removed by mistake, for example, removing access from a user
# and giving it back again.
WAIT_FOR_DELETE = 1.hour
ASSIGNED = 1
MENTIONED = 2
BUILD_FAILED = 3
......
......@@ -11,7 +11,7 @@ class IssuablePolicy < BasePolicy
@user && @subject.assignee_or_author?(@user)
end
rule { assignee_or_author }.policy do
rule { can?(:guest_access) & assignee_or_author }.policy do
enable :read_issue
enable :update_issue
enable :reopen_issue
......
......@@ -31,7 +31,7 @@ module Groups
def after_update
if group.previous_changes.include?(:visibility_level) && group.private?
# don't enqueue immediately to prevent todos removal in case of a mistake
TodosDestroyer::GroupPrivateWorker.perform_in(1.hour, group.id)
TodosDestroyer::GroupPrivateWorker.perform_in(Todo::WAIT_FOR_DELETE, group.id)
end
end
......
......@@ -38,7 +38,7 @@ module Issues
if issue.previous_changes.include?('confidential')
# don't enqueue immediately to prevent todos removal in case of a mistake
TodosDestroyer::ConfidentialIssueWorker.perform_in(1.hour, issue.id) if issue.confidential?
TodosDestroyer::ConfidentialIssueWorker.perform_in(Todo::WAIT_FOR_DELETE, issue.id) if issue.confidential?
create_confidentiality_note(issue)
end
......
......@@ -47,5 +47,11 @@ module Members
raise "Unknown action '#{action}' on #{member}!"
end
end
def enqueue_delete_todos(member)
type = member.is_a?(GroupMember) ? 'Group' : 'Project'
# don't enqueue immediately to prevent todos removal in case of a mistake
TodosDestroyer::EntityLeaveWorker.perform_in(Todo::WAIT_FOR_DELETE, member.user_id, member.source_id, type)
end
end
end
......@@ -15,7 +15,7 @@ module Members
notification_service.decline_access_request(member)
end
enqeue_delete_todos(member)
enqueue_delete_todos(member)
after_execute(member: member)
......@@ -24,12 +24,6 @@ module Members
private
def enqeue_delete_todos(member)
type = member.is_a?(GroupMember) ? 'Group' : 'Project'
# don't enqueue immediately to prevent todos removal in case of a mistake
TodosDestroyer::EntityLeaveWorker.perform_in(1.hour, member.user_id, member.source_id, type)
end
def can_destroy_member?(member)
can?(current_user, destroy_member_permission(member), member)
end
......
......@@ -10,9 +10,18 @@ module Members
if member.update(params)
after_execute(action: permission, old_access_level: old_access_level, member: member)
# Deletes only confidential issues todos for guests
enqueue_delete_todos(member) if downgrading_to_guest?
end
member
end
private
def downgrading_to_guest?
params[:access_level] == Gitlab::Access::GUEST
end
end
end
......@@ -17,7 +17,7 @@ module MergeRequests
merge_request.source_project = find_source_project
merge_request.target_project = find_target_project
merge_request.target_branch = find_target_branch
merge_request.can_be_created = branches_valid?
merge_request.can_be_created = projects_and_branches_valid?
# compare branches only if branches are valid, otherwise
# compare_branches may raise an error
......@@ -48,15 +48,19 @@ module MergeRequests
to: :merge_request
def find_source_project
return source_project if source_project.present? && can?(current_user, :read_project, source_project)
return source_project if source_project.present? && can?(current_user, :create_merge_request_from, source_project)
project
end
def find_target_project
return target_project if target_project.present? && can?(current_user, :read_project, target_project)
return target_project if target_project.present? && can?(current_user, :create_merge_request_in, target_project)
project.default_merge_request_target
target_project = project.default_merge_request_target
return target_project if target_project.present? && can?(current_user, :create_merge_request_in, target_project)
project
end
def find_target_branch
......@@ -71,10 +75,11 @@ module MergeRequests
params[:target_branch].present?
end
def branches_valid?
def projects_and_branches_valid?
return false if source_project.nil? || target_project.nil?
return false unless source_branch_specified? || target_branch_specified?
validate_branches
validate_projects_and_branches
errors.blank?
end
......@@ -93,7 +98,12 @@ module MergeRequests
end
end
def validate_branches
def validate_projects_and_branches
merge_request.validate_target_project
merge_request.validate_fork
return if errors.any?
add_error('You must select source and target branch') unless branches_present?
add_error('You must select different branches') if same_source_and_target?
add_error("Source branch \"#{source_branch}\" does not exist") unless source_branch_exists?
......
......@@ -12,28 +12,43 @@ module Projects
return if LfsObject.exists?(oid: oid)
sanitized_uri = Gitlab::UrlSanitizer.new(url)
Gitlab::UrlBlocker.validate!(sanitized_uri.sanitized_url, protocols: VALID_PROTOCOLS)
sanitized_uri = sanitize_url!(url)
with_tmp_file(oid) do |file|
size = download_and_save_file(file, sanitized_uri)
lfs_object = LfsObject.new(oid: oid, size: size, file: file)
download_and_save_file(file, sanitized_uri)
lfs_object = LfsObject.new(oid: oid, size: file.size, file: file)
project.all_lfs_objects << lfs_object
end
rescue Gitlab::UrlBlocker::BlockedUrlError => e
Rails.logger.error("LFS file with oid #{oid} couldn't be downloaded: #{e.message}")
rescue StandardError => e
Rails.logger.error("LFS file with oid #{oid} could't be downloaded from #{sanitized_uri.sanitized_url}: #{e.message}")
Rails.logger.error("LFS file with oid #{oid} couldn't be downloaded from #{sanitized_uri.sanitized_url}: #{e.message}")
end
# rubocop: enable CodeReuse/ActiveRecord
private
def sanitize_url!(url)
Gitlab::UrlSanitizer.new(url).tap do |sanitized_uri|
# Just validate that HTTP/HTTPS protocols are used. The
# subsequent Gitlab::HTTP.get call will do network checks
# based on the settings.
Gitlab::UrlBlocker.validate!(sanitized_uri.sanitized_url,
protocols: VALID_PROTOCOLS)
end
end
def download_and_save_file(file, sanitized_uri)
IO.copy_stream(open(sanitized_uri.sanitized_url, headers(sanitized_uri)), file) # rubocop:disable Security/Open
response = Gitlab::HTTP.get(sanitized_uri.sanitized_url, headers(sanitized_uri)) do |fragment|
file.write(fragment)
end
raise StandardError, "Received error code #{response.code}" unless response.success?
end
def headers(sanitized_uri)
{}.tap do |headers|
query_options.tap do |headers|
credentials = sanitized_uri.credentials
if credentials[:user].present? || credentials[:password].present?
......@@ -43,10 +58,14 @@ module Projects
end
end
def query_options
{ stream_body: true }
end
def with_tmp_file(oid)
create_tmp_storage_dir
File.open(File.join(tmp_storage_dir, oid), 'w') { |file| yield file }
File.open(File.join(tmp_storage_dir, oid), 'wb') { |file| yield file }
end
def create_tmp_storage_dir
......
......@@ -61,9 +61,9 @@ module Projects
if project.previous_changes.include?(:visibility_level) && project.private?
# don't enqueue immediately to prevent todos removal in case of a mistake
TodosDestroyer::ProjectPrivateWorker.perform_in(1.hour, project.id)
TodosDestroyer::ProjectPrivateWorker.perform_in(Todo::WAIT_FOR_DELETE, project.id)
elsif (project_changed_feature_keys & todos_features_changes).present?
TodosDestroyer::PrivateFeaturesWorker.perform_in(1.hour, project.id)
TodosDestroyer::PrivateFeaturesWorker.perform_in(Todo::WAIT_FOR_DELETE, project.id)
end
if project.previous_changes.include?('path')
......
---
title: Escape html entities in LabelReferenceFilter when no label found
merge_request:
author:
type: security
---
title: Prevent a path traversal attack on global file templates
merge_request:
author:
type: security
---
title: Ensure that build token is only used when running
merge_request:
author:
type: security
---
title: Escape label and milestone titles to prevent XSS in GFM autocomplete
merge_request: 2741
author:
type: security
---
title: Allow changing group CI/CD settings only for owners.
merge_request:
author:
type: security
---
title: Authorize before reading job information via API.
merge_request:
author:
type: security
---
title: Prevent leaking protected variables for ambiguous refs.
merge_request:
author:
type: security
---
title: Validate LFS hrefs before downloading them
merge_request:
author:
type: security
---
title: Issuable no longer is visible to users when project can't be viewed
merge_request:
author:
type: security
---
title: Don't expose cross project repositories through diffs when creating merge reqeusts
merge_request:
author:
type: security
---
title: Fix SSRF with import_url and remote mirror url
merge_request:
author:
type: security
---
title: Fix persistent symlink in project import
merge_request:
author:
type: security
---
title: Set URL rel attribute for broken URLs.
merge_request:
author:
type: security
---
title: Project guests no longer are able to see refs page
merge_request:
author:
type: security
---
title: Delete confidential todos for user when downgraded to Guest
merge_request:
author:
type: security
......@@ -35,6 +35,9 @@ A Todo appears in your Todos dashboard when:
- the author, or
- have set it to automatically merge once pipeline succeeds.
NOTE: **Note:**
When an user no longer has access to a resource related to a Todo like an issue, merge request, project or group the related Todos, for security reasons, gets deleted within the next hour. The delete is delayed to prevent data loss in case user got their access revoked by mistake.
### Directly addressed Todos
> [Introduced][ce-7926] in GitLab 9.0.
......
......@@ -1346,7 +1346,17 @@ module API
end
class Dependency < Grape::Entity
expose :id, :name, :token
expose :id, :name
expose :token do |dependency, options|
# overrides the job's dependency authorization token
# with the token of the job that is being run
# this way we use the parent job auth token
#
# ideally we would change the runner implementation to
# use different token, but this would require upgrade of
# all runners which is impossible
options[:auth_token]
end
expose :artifacts_file, using: JobArtifactFile, if: ->(job, _) { job.artifacts? }
end
......@@ -1374,7 +1384,10 @@ module API
expose :artifacts, using: Artifacts
expose :cache, using: Cache
expose :credentials, using: Credentials
expose :dependencies, using: Dependency
expose :dependencies do |model|
Dependency.represent(model.dependencies,
options.merge(auth_token: model.token))
end
expose :features
end
end
......
......@@ -36,26 +36,32 @@ module API
def validate_job!(job)
not_found! unless job
yield if block_given?
project = job.project
forbidden!('Project has been deleted!') if project.nil? || project.pending_delete?
forbidden!('Job has been erased!') if job.erased?
job_forbidden!(job, 'Project has been deleted!') if project.nil? || project.pending_delete?
job_forbidden!(job, 'Job has been erased!') if job.erased?
job_forbidden!(job, 'Not running!') unless job.running?
end
def authenticate_job!
job = Ci::Build.find_by_id(params[:id])
def authenticate_job_by_token!
token = (params[JOB_TOKEN_PARAM] || env[JOB_TOKEN_HEADER]).to_s
validate_job!(job) do
forbidden! unless job_token_valid?(job)
Ci::Build.find_by_token(token).tap do |job|
validate_job!(job)
end
end
job
# we look for a job that has ID and token matching
def authenticate_job!
authenticate_job_by_token!.tap do |job|
job_forbidden!(job, 'Invalid Job ID!') unless job.id == params[:id]
end
end
def job_token_valid?(job)
token = (params[JOB_TOKEN_PARAM] || env[JOB_TOKEN_HEADER]).to_s
token && job.valid_token?(token)
# we look for a job that has been shared via pipeline using the ID
def authenticate_pipeline_job!
job = authenticate_job_by_token!
job.pipeline.builds.find(params[:id])
end
def max_artifacts_size
......
......@@ -38,6 +38,8 @@ module API
end
# rubocop: disable CodeReuse/ActiveRecord
get ':id/jobs' do
authorize_read_builds!
builds = user_project.builds.order('id DESC')
builds = filter_builds(builds, params[:scope])
......@@ -56,7 +58,10 @@ module API
end
# rubocop: disable CodeReuse/ActiveRecord
get ':id/pipelines/:pipeline_id/jobs' do
authorize!(:read_pipeline, user_project)
pipeline = user_project.pipelines.find(params[:pipeline_id])
authorize!(:read_build, pipeline)
builds = pipeline.builds
builds = filter_builds(builds, params[:scope])
builds = builds.preload(:job_artifacts_archive, :job_artifacts, project: [:namespace])
......
......@@ -146,7 +146,6 @@ module API
end
put '/:id' do