...@@ -2,6 +2,33 @@ ...@@ -2,6 +2,33 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. entry.
   
## 12.8.2
### Security (17 changes)
- Update container registry authentication to account for login request when checking permissions.
- Update ProjectAuthorization when deleting or updating GroupGroupLink.
- Prevent an endless checking loop for two merge requests targeting each other.
- Update user 2fa when accepting a group invite.
- Fix for XSS in branch names.
- Prevent directory traversal through FileUploader.
- Run project badge images through the asset proxy.
- Check merge requests read permissions before showing them in the pipeline widget.
- Respect member access level for group shares.
- Remove OID filtering during LFS imports.
- Protect against denial of service using pipeline webhook recursion.
- Expire account confirmation token.
- Prevent XSS in admin grafana URL setting.
- Don't require base_sha in DiffRefsType.
- Sanitize output by dependency linkers.
- Recalculate ProjectAuthorizations for all users.
- Escape special chars in Sentry error header.
### Other (1 change, 1 of them is from the community)
- Fix fixtures for Error Tracking Web UI. !26233 (Takuya Noguchi)
## 12.8.1 ## 12.8.1
   
### Fixed (5 changes) ### Fixed (5 changes)
... ...
......
12.8.1 12.8.2
12.8.1 12.8.2
...@@ -104,16 +104,6 @@ export default { ...@@ -104,16 +104,6 @@ export default {
'errorStatus', 'errorStatus',
]), ]),
...mapGetters('details', ['stacktrace']), ...mapGetters('details', ['stacktrace']),
reported() {
return sprintf(
__('Reported %{timeAgo} by %{reportedBy}'),
{
reportedBy: `<strong>${this.error.culprit}</strong>`,
timeAgo: this.timeFormatted(this.stacktraceData.date_received),
},
false,
);
},
firstReleaseLink() { firstReleaseLink() {
return `${this.error.externalBaseUrl}/releases/${this.error.firstReleaseShortVersion}`; return `${this.error.externalBaseUrl}/releases/${this.error.firstReleaseShortVersion}`;
}, },
...@@ -218,7 +208,17 @@ export default { ...@@ -218,7 +208,17 @@ export default {
</gl-alert> </gl-alert>
<div class="top-area align-items-center justify-content-between py-3"> <div class="top-area align-items-center justify-content-between py-3">
<span v-if="!loadingStacktrace && stacktrace" v-html="reported"></span> <div v-if="!loadingStacktrace && stacktrace" data-qa-selector="reported_text">
<gl-sprintf :message="__('Reported %{timeAgo} by %{reportedBy}')">
<template #reportedBy>
<strong>{{ error.culprit }}</strong>
</template>
<template #timeAgo>
{{ timeFormatted(stacktraceData.date_received) }}
</template>
</gl-sprintf>
</div>
<div class="d-inline-flex ml-lg-auto"> <div class="d-inline-flex ml-lg-auto">
<loading-button <loading-button
:label="ignoreBtnLabel" :label="ignoreBtnLabel"
... ...
......
<script> <script>
import { GlLoadingIcon } from '@gitlab/ui'; import { GlLoadingIcon } from '@gitlab/ui';
import { escape } from 'lodash';
import simplePoll from '../../../lib/utils/simple_poll'; import simplePoll from '../../../lib/utils/simple_poll';
import eventHub from '../../event_hub'; import eventHub from '../../event_hub';
import statusIcon from '../mr_widget_status_icon.vue'; import statusIcon from '../mr_widget_status_icon.vue';
...@@ -44,11 +45,10 @@ export default { ...@@ -44,11 +45,10 @@ export default {
fastForwardMergeText() { fastForwardMergeText() {
return sprintf( return sprintf(
__( __(
`Fast-forward merge is not possible. Rebase the source branch onto %{startTag}${this.mr.targetBranch}%{endTag} to allow this merge request to be merged.`, 'Fast-forward merge is not possible. Rebase the source branch onto %{targetBranch} to allow this merge request to be merged.',
), ),
{ {
startTag: '<span class="label-branch">', targetBranch: `<span class="label-branch">${escape(this.mr.targetBranch)}</span>`,
endTag: '</span>',
}, },
false, false,
); );
... ...
......
...@@ -9,6 +9,7 @@ module UploadsActions ...@@ -9,6 +9,7 @@ module UploadsActions
included do included do
prepend_before_action :set_request_format_from_path_extension prepend_before_action :set_request_format_from_path_extension
rescue_from FileUploader::InvalidSecret, with: :render_404
end end
def create def create
... ...
......
...@@ -24,7 +24,7 @@ class Groups::GroupLinksController < Groups::ApplicationController ...@@ -24,7 +24,7 @@ class Groups::GroupLinksController < Groups::ApplicationController
end end
def update def update
@group_link.update(group_link_params) Groups::GroupLinks::UpdateService.new(@group_link).execute(group_link_params)
end end
def destroy def destroy
... ...
......
...@@ -8,7 +8,7 @@ module Types ...@@ -8,7 +8,7 @@ module Types
field :head_sha, GraphQL::STRING_TYPE, null: false, field :head_sha, GraphQL::STRING_TYPE, null: false,
description: 'SHA of the HEAD at the time the comment was made' description: 'SHA of the HEAD at the time the comment was made'
field :base_sha, GraphQL::STRING_TYPE, null: false, field :base_sha, GraphQL::STRING_TYPE, null: true,
description: 'Merge base of the branch the comment was made on' description: 'Merge base of the branch the comment was made on'
field :start_sha, GraphQL::STRING_TYPE, null: false, field :start_sha, GraphQL::STRING_TYPE, null: false,
description: 'SHA of the branch being compared against' description: 'SHA of the branch being compared against'
... ...
......
...@@ -6,6 +6,9 @@ class ApplicationSetting < ApplicationRecord ...@@ -6,6 +6,9 @@ class ApplicationSetting < ApplicationRecord
include TokenAuthenticatable include TokenAuthenticatable
include ChronicDurationAttribute include ChronicDurationAttribute
GRAFANA_URL_ERROR_MESSAGE = 'Please check your Grafana URL setting in ' \
'Admin Area > Settings > Metrics and profiling > Metrics - Grafana'
add_authentication_token_field :runners_registration_token, encrypted: -> { Feature.enabled?(:application_settings_tokens_optional_encryption, default_enabled: true) ? :optional : :required } add_authentication_token_field :runners_registration_token, encrypted: -> { Feature.enabled?(:application_settings_tokens_optional_encryption, default_enabled: true) ? :optional : :required }
add_authentication_token_field :health_check_access_token add_authentication_token_field :health_check_access_token
add_authentication_token_field :static_objects_external_storage_auth_token add_authentication_token_field :static_objects_external_storage_auth_token
...@@ -38,6 +41,14 @@ class ApplicationSetting < ApplicationRecord ...@@ -38,6 +41,14 @@ class ApplicationSetting < ApplicationRecord
chronic_duration_attr_writer :archive_builds_in_human_readable, :archive_builds_in_seconds chronic_duration_attr_writer :archive_builds_in_human_readable, :archive_builds_in_seconds
validates :grafana_url,
system_hook_url: {
blocked_message: "is blocked: %{exception_message}. " + GRAFANA_URL_ERROR_MESSAGE
},
if: :grafana_url_absolute?
validate :validate_grafana_url
validates :uuid, presence: true validates :uuid, presence: true
validates :outbound_local_requests_whitelist, validates :outbound_local_requests_whitelist,
...@@ -357,6 +368,19 @@ class ApplicationSetting < ApplicationRecord ...@@ -357,6 +368,19 @@ class ApplicationSetting < ApplicationRecord
end end
after_commit :expire_performance_bar_allowed_user_ids_cache, if: -> { previous_changes.key?('performance_bar_allowed_group_id') } after_commit :expire_performance_bar_allowed_user_ids_cache, if: -> { previous_changes.key?('performance_bar_allowed_group_id') }
def validate_grafana_url
unless parsed_grafana_url
self.errors.add(
:grafana_url,
"must be a valid relative or absolute URL. #{GRAFANA_URL_ERROR_MESSAGE}"
)
end
end
def grafana_url_absolute?
parsed_grafana_url&.absolute?
end
def sourcegraph_url_is_com? def sourcegraph_url_is_com?
!!(sourcegraph_url =~ /\Ahttps:\/\/(www\.)?sourcegraph\.com/) !!(sourcegraph_url =~ /\Ahttps:\/\/(www\.)?sourcegraph\.com/)
end end
...@@ -381,6 +405,12 @@ class ApplicationSetting < ApplicationRecord ...@@ -381,6 +405,12 @@ class ApplicationSetting < ApplicationRecord
def recaptcha_or_login_protection_enabled def recaptcha_or_login_protection_enabled
recaptcha_enabled || login_recaptcha_protection_enabled recaptcha_enabled || login_recaptcha_protection_enabled
end end
private
def parsed_grafana_url
@parsed_grafana_url ||= Gitlab::Utils.parse_url(grafana_url)
end
end end
ApplicationSetting.prepend_if_ee('EE::ApplicationSetting') ApplicationSetting.prepend_if_ee('EE::ApplicationSetting')
...@@ -32,7 +32,9 @@ class Badge < ApplicationRecord ...@@ -32,7 +32,9 @@ class Badge < ApplicationRecord
end end
def rendered_image_url(project = nil) def rendered_image_url(project = nil)
Gitlab::AssetProxy.proxy_url(
build_rendered_url(image_url, project) build_rendered_url(image_url, project)
)
end end
private private
... ...
......
...@@ -509,18 +509,29 @@ class Group < Namespace ...@@ -509,18 +509,29 @@ class Group < Namespace
group_group_links_query = GroupGroupLink.where(shared_group_id: self_and_ancestors_ids) group_group_links_query = GroupGroupLink.where(shared_group_id: self_and_ancestors_ids)
cte = Gitlab::SQL::CTE.new(:group_group_links_cte, group_group_links_query) cte = Gitlab::SQL::CTE.new(:group_group_links_cte, group_group_links_query)
cte_alias = cte.table.alias(GroupGroupLink.table_name)
link = GroupGroupLink link = GroupGroupLink
.with(cte.to_arel) .with(cte.to_arel)
.select(smallest_value_arel([cte_alias[:group_access], group_member_table[:access_level]],
'group_access'))
.from([group_member_table, cte.alias_to(group_group_link_table)]) .from([group_member_table, cte.alias_to(group_group_link_table)])
.where(group_member_table[:user_id].eq(user.id)) .where(group_member_table[:user_id].eq(user.id))
.where(group_member_table[:requested_at].eq(nil))
.where(group_member_table[:source_id].eq(group_group_link_table[:shared_with_group_id])) .where(group_member_table[:source_id].eq(group_group_link_table[:shared_with_group_id]))
.where(group_member_table[:source_type].eq('Namespace'))
.reorder(Arel::Nodes::Descending.new(group_group_link_table[:group_access])) .reorder(Arel::Nodes::Descending.new(group_group_link_table[:group_access]))
.first .first
link&.group_access link&.group_access
end end
def smallest_value_arel(args, column_alias)
Arel::Nodes::As.new(
Arel::Nodes::NamedFunction.new('LEAST', args),
Arel::Nodes::SqlLiteral.new(column_alias))
end
def self.groups_including_descendants_by(group_ids) def self.groups_including_descendants_by(group_ids)
Gitlab::ObjectHierarchy Gitlab::ObjectHierarchy
.new(Group.where(id: group_ids)) .new(Group.where(id: group_ids))
... ...
......
...@@ -66,6 +66,7 @@ class GroupMember < Member ...@@ -66,6 +66,7 @@ class GroupMember < Member
def after_accept_invite def after_accept_invite
notification_service.accept_group_invite(self) notification_service.accept_group_invite(self)
update_two_factor_requirement
super super
end end
... ...
......
...@@ -134,7 +134,11 @@ module Ci ...@@ -134,7 +134,11 @@ module Ci
def all_related_merge_requests def all_related_merge_requests
strong_memoize(:all_related_merge_requests) do strong_memoize(:all_related_merge_requests) do
pipeline.ref ? pipeline.all_merge_requests_by_recency.to_a : [] if pipeline.ref && can?(current_user, :read_merge_request, pipeline.project)
pipeline.all_merge_requests_by_recency.to_a
else
[]
end
end end
end end
end end
... ...
......
...@@ -3,12 +3,24 @@ ...@@ -3,12 +3,24 @@
module Auth module Auth
class ContainerRegistryAuthenticationService < BaseService class ContainerRegistryAuthenticationService < BaseService
AUDIENCE = 'container_registry' AUDIENCE = 'container_registry'
REGISTRY_LOGIN_ABILITIES = [
:read_container_image,
:create_container_image,
:destroy_container_image,
:update_container_image,
:admin_container_image,
:build_read_container_image,
:build_create_container_image,
:build_destroy_container_image
].freeze
def execute(authentication_abilities:) def execute(authentication_abilities:)
@authentication_abilities = authentication_abilities @authentication_abilities = authentication_abilities
return error('UNAVAILABLE', status: 404, message: 'registry not enabled') unless registry.enabled return error('UNAVAILABLE', status: 404, message: 'registry not enabled') unless registry.enabled
return error('DENIED', status: 403, message: 'access forbidden') unless has_registry_ability?
unless scopes.any? || current_user || project unless scopes.any? || current_user || project
return error('DENIED', status: 403, message: 'access forbidden') return error('DENIED', status: 403, message: 'access forbidden')
end end
...@@ -197,5 +209,11 @@ module Auth ...@@ -197,5 +209,11 @@ module Auth
def has_authentication_ability?(capability) def has_authentication_ability?(capability)
@authentication_abilities.to_a.include?(capability) @authentication_abilities.to_a.include?(capability)
end end
def has_registry_ability?
@authentication_abilities.any? do |ability|
REGISTRY_LOGIN_ABILITIES.include?(ability)
end
end
end end
end end
...@@ -6,19 +6,17 @@ module Groups ...@@ -6,19 +6,17 @@ module Groups
def execute(one_or_more_links) def execute(one_or_more_links)
links = Array(one_or_more_links) links = Array(one_or_more_links)
GroupGroupLink.transaction do if GroupGroupLink.delete(links)
GroupGroupLink.delete(links) Gitlab::AppLogger.info(
"GroupGroupLinks with ids: #{links.map(&:id)} have been deleted.")
groups_to_refresh = links.map(&:shared_with_group) groups_to_refresh = links.map(&:shared_with_group)
groups_to_refresh.uniq.each do |group| groups_to_refresh.uniq.each do |group|
group.refresh_members_authorized_projects group.refresh_members_authorized_projects
end end
else
Gitlab::AppLogger.info("GroupGroupLinks with ids: #{links.map(&:id)} have been deleted.") Gitlab::AppLogger.info(
rescue => ex "Failed to delete GroupGroupLinks with ids: #{links.map(&:id)}.")
Gitlab::AppLogger.error(ex)
raise
end end
end end
end end
... ...
......
# frozen_string_literal: true
module Groups
module GroupLinks
class UpdateService < BaseService
def initialize(group_link, user = nil)
super(group_link.shared_group, user)
@group_link = group_link
end
def execute(group_link_params)
group_link.update!(group_link_params)
if requires_authorization_refresh?(group_link_params)
group_link.shared_with_group.refresh_members_authorized_projects
end
end
private
attr_accessor :group_link
def requires_authorization_refresh?(params)
params.include?(:group_access)
end
end
end
end
...@@ -16,17 +16,14 @@ module Projects ...@@ -16,17 +16,14 @@ module Projects
@lfs_download_object = lfs_download_object @lfs_download_object = lfs_download_object
end end
# rubocop: disable CodeReuse/ActiveRecord
def execute def execute
return unless project&.lfs_enabled? && lfs_download_object return unless project&.lfs_enabled? && lfs_download_object
return error("LFS file with oid #{lfs_oid} has invalid attributes") unless lfs_download_object.valid? return error("LFS file with oid #{lfs_oid} has invalid attributes") unless lfs_download_object.valid?
return if LfsObject.exists?(oid: lfs_oid)
wrap_download_errors do wrap_download_errors do
download_lfs_file! download_lfs_file!
end end
end end
# rubocop: enable CodeReuse/ActiveRecord
private private
...@@ -39,14 +36,24 @@ module Projects ...@@ -39,14 +36,24 @@ module Projects
def download_lfs_file! def download_lfs_file!
with_tmp_file do |tmp_file| with_tmp_file do |tmp_file|
download_and_save_file!(tmp_file) download_and_save_file!(tmp_file)
project.lfs_objects << LfsObject.new(oid: lfs_oid,
size: lfs_size, project.lfs_objects << find_or_create_lfs_object(tmp_file)
file: tmp_file)
success success
end end
end end
def find_or_create_lfs_object(tmp_file)
lfs_obj = LfsObject.safe_find_or_create_by!(
oid: lfs_oid,
size: lfs_size
)
lfs_obj.update!(file: tmp_file) unless lfs_obj.file.file
lfs_obj
end
def download_and_save_file!(file) def download_and_save_file!(file)
digester = Digest::SHA256.new digester = Digest::SHA256.new
response = Gitlab::HTTP.get(lfs_sanitized_url, download_headers) do |fragment| response = Gitlab::HTTP.get(lfs_sanitized_url, download_headers) do |fragment|
... ...
......
...@@ -26,12 +26,12 @@ module Projects ...@@ -26,12 +26,12 @@ module Projects
return [] return []
end end
# Getting all Lfs pointers already in the database and linking them to the project # Downloading the required information and gathering it inside an
linked_oids = LfsLinkService.new(project).execute(lfs_pointers_in_repository.keys) # LfsDownloadObject for each oid
# Retrieving those oids not present in the database which we need to download #
missing_oids = lfs_pointers_in_repository.except(*linked_oids) LfsDownloadLinkListService
# Downloading the required information and gathering it inside a LfsDownloadObject for each oid .new(project, remote_uri: current_endpoint_uri)
LfsDownloadLinkListService.new(project, remote_uri: current_endpoint_uri).execute(missing_oids) .execute(lfs_pointers_in_repository)
rescue LfsDownloadLinkListService::DownloadLinksError => e rescue LfsDownloadLinkListService::DownloadLinksError => e
raise LfsObjectDownloadListError, "The LFS objects download list couldn't be imported. Error: #{e.message}" raise LfsObjectDownloadListError, "The LFS objects download list couldn't be imported. Error: #{e.message}"
end end
... ...
......
...@@ -13,8 +13,14 @@ class WebHookService ...@@ -13,8 +13,14 @@ class WebHookService
end end
end end
GITLAB_EVENT_HEADER = 'X-Gitlab-Event'
attr_accessor :hook, :data, :hook_name, :request_options attr_accessor :hook, :data, :hook_name, :request_options
def self.hook_to_event(hook_name)
hook_name.to_s.singularize.titleize
end
def initialize(hook, data, hook_name) def initialize(hook, data, hook_name)
@hook = hook @hook = hook
@data = data @data = data
...@@ -112,7 +118,7 @@ class WebHookService ...@@ -112,7 +118,7 @@ class WebHookService
@headers ||= begin @headers ||= begin
{ {
'Content-Type' => 'application/json', 'Content-Type' => 'application/json',
'X-Gitlab-Event' => hook_name.singularize.titleize GITLAB_EVENT_HEADER => self.class.hook_to_event(hook_name)
}.tap do |hash| }.tap do |hash|
hash['X-Gitlab-Token'] = Gitlab::Utils.remove_line_breaks(hook.token) if hook.token.present? hash['X-Gitlab-Token'] = Gitlab::Utils.remove_line_breaks(hook.token) if hook.token.present?
end end
... ...
......
...@@ -16,6 +16,9 @@ class FileUploader < GitlabUploader ...@@ -16,6 +16,9 @@ class FileUploader < GitlabUploader
MARKDOWN_PATTERN = %r{\!?\[.*?\]\(/uploads/(?<secret>[0-9a-f]{32})/(?<file>.*?)\)}.freeze MARKDOWN_PATTERN = %r{\!?\[.*?\]\(/uploads/(?<secret>[0-9a-f]{32})/(?<file>.*?)\)}.freeze
DYNAMIC_PATH_PATTERN = %r{.*(?<secret>\h{32})/(?<identifier>.*)}.freeze DYNAMIC_PATH_PATTERN = %r{.*(?<secret>\h{32})/(?<identifier>.*)}.freeze
VALID_SECRET_PATTERN = %r{\A\h{10,32}\z}.freeze
InvalidSecret = Class.new(StandardError)
after :remove, :prune_store_dir after :remove, :prune_store_dir
...@@ -153,6 +156,10 @@ class FileUploader < GitlabUploader ...@@ -153,6 +156,10 @@ class FileUploader < GitlabUploader
def secret def secret
@secret ||= self.class.generate_secret @secret ||= self.class.generate_secret
raise InvalidSecret unless @secret =~ VALID_SECRET_PATTERN
@secret
end end
# return a new uploader with a file copy on another project # return a new uploader with a file copy on another project
... ...
......