From 1ebdda69d61ae26379f8fac27671103374031944 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Mon, 31 Jul 2023 14:35:12 +0000 Subject: Add latest changes from gitlab-org/security/gitlab@16-2-stable-ee --- .../common/pipelines/lfs_objects_pipeline.rb | 2 +- .../common/pipelines/uploads_pipeline.rb | 2 +- lib/bulk_imports/file_downloads/validations.rb | 2 +- .../projects/pipelines/design_bundle_pipeline.rb | 2 +- .../pipelines/repository_bundle_pipeline.rb | 2 +- lib/gitlab/ci/decompressed_gzip_size_validator.rb | 2 +- lib/gitlab/import_export/command_line_util.rb | 33 +++++++++++++------- .../decompressed_archive_size_validator.rb | 2 +- lib/gitlab/import_export/file_importer.rb | 4 +-- lib/gitlab/import_export/json/ndjson_reader.rb | 6 ++-- .../import_export/recursive_merge_folders.rb | 2 +- lib/gitlab/pages/virtual_host_finder.rb | 5 ++-- lib/gitlab/utils/file_info.rb | 35 ++++++++++++++++++++++ 13 files changed, 74 insertions(+), 25 deletions(-) create mode 100644 lib/gitlab/utils/file_info.rb (limited to 'lib') diff --git a/lib/bulk_imports/common/pipelines/lfs_objects_pipeline.rb b/lib/bulk_imports/common/pipelines/lfs_objects_pipeline.rb index 0bf4d341aad..bd09b6add00 100644 --- a/lib/bulk_imports/common/pipelines/lfs_objects_pipeline.rb +++ b/lib/bulk_imports/common/pipelines/lfs_objects_pipeline.rb @@ -26,7 +26,7 @@ module BulkImports return if tar_filepath?(file_path) return if lfs_json_filepath?(file_path) return if File.directory?(file_path) - return if File.lstat(file_path).symlink? + return if Gitlab::Utils::FileInfo.linked?(file_path) size = File.size(file_path) oid = LfsObject.calculate_oid(file_path) diff --git a/lib/bulk_imports/common/pipelines/uploads_pipeline.rb b/lib/bulk_imports/common/pipelines/uploads_pipeline.rb index 81ce20db9ab..ea17af36c9a 100644 --- a/lib/bulk_imports/common/pipelines/uploads_pipeline.rb +++ b/lib/bulk_imports/common/pipelines/uploads_pipeline.rb @@ -26,7 +26,7 @@ module BulkImports # Validate that the path is OK to load Gitlab::PathTraversal.check_allowed_absolute_path_and_path_traversal!(file_path, [Dir.tmpdir]) return if File.directory?(file_path) - return if File.lstat(file_path).symlink? + return if Gitlab::Utils::FileInfo.linked?(file_path) avatar_path = AVATAR_PATTERN.match(file_path) return save_avatar(file_path) if avatar_path diff --git a/lib/bulk_imports/file_downloads/validations.rb b/lib/bulk_imports/file_downloads/validations.rb index b852a50c888..e1844843408 100644 --- a/lib/bulk_imports/file_downloads/validations.rb +++ b/lib/bulk_imports/file_downloads/validations.rb @@ -32,7 +32,7 @@ module BulkImports end def validate_symlink - return unless File.lstat(filepath).symlink? + return unless Gitlab::Utils::FileInfo.linked?(filepath) File.delete(filepath) raise_error 'Invalid downloaded file' diff --git a/lib/bulk_imports/projects/pipelines/design_bundle_pipeline.rb b/lib/bulk_imports/projects/pipelines/design_bundle_pipeline.rb index 373cd2bd75a..235d2629b9e 100644 --- a/lib/bulk_imports/projects/pipelines/design_bundle_pipeline.rb +++ b/lib/bulk_imports/projects/pipelines/design_bundle_pipeline.rb @@ -26,7 +26,7 @@ module BulkImports return unless portable.lfs_enabled? return unless File.exist?(bundle_path) return if File.directory?(bundle_path) - return if File.lstat(bundle_path).symlink? + return if Gitlab::Utils::FileInfo.linked?(bundle_path) portable.design_repository.create_from_bundle(bundle_path) end diff --git a/lib/bulk_imports/projects/pipelines/repository_bundle_pipeline.rb b/lib/bulk_imports/projects/pipelines/repository_bundle_pipeline.rb index f19d8931f4a..4307cb2bafd 100644 --- a/lib/bulk_imports/projects/pipelines/repository_bundle_pipeline.rb +++ b/lib/bulk_imports/projects/pipelines/repository_bundle_pipeline.rb @@ -26,7 +26,7 @@ module BulkImports return unless File.exist?(bundle_path) return if File.directory?(bundle_path) - return if File.lstat(bundle_path).symlink? + return if Gitlab::Utils::FileInfo.linked?(bundle_path) portable.repository.create_from_bundle(bundle_path) end diff --git a/lib/gitlab/ci/decompressed_gzip_size_validator.rb b/lib/gitlab/ci/decompressed_gzip_size_validator.rb index 9b7b5f0dd66..b386e400423 100644 --- a/lib/gitlab/ci/decompressed_gzip_size_validator.rb +++ b/lib/gitlab/ci/decompressed_gzip_size_validator.rb @@ -65,7 +65,7 @@ module Gitlab def validate_archive_path Gitlab::PathTraversal.check_path_traversal!(archive_path) - raise(ServiceError, 'Archive path is a symlink') if File.lstat(archive_path).symlink? + raise(ServiceError, 'Archive path is a symlink or hard link') if Gitlab::Utils::FileInfo.linked?(archive_path) raise(ServiceError, 'Archive path is not a file') unless File.file?(archive_path) end diff --git a/lib/gitlab/import_export/command_line_util.rb b/lib/gitlab/import_export/command_line_util.rb index d681f39f00b..e2f365fcbf8 100644 --- a/lib/gitlab/import_export/command_line_util.rb +++ b/lib/gitlab/import_export/command_line_util.rb @@ -5,8 +5,11 @@ module Gitlab module CommandLineUtil UNTAR_MASK = 'u+rwX,go+rX,go-w' DEFAULT_DIR_MODE = 0700 + CLEAN_DIR_IGNORE_FILE_NAMES = %w[. ..].freeze - FileOversizedError = Class.new(StandardError) + CommandLineUtilError = Class.new(StandardError) + FileOversizedError = Class.new(CommandLineUtilError) + HardLinkError = Class.new(CommandLineUtilError) def tar_czf(archive:, dir:) tar_with_options(archive: archive, dir: dir, options: 'czf') @@ -90,7 +93,7 @@ module Gitlab def untar_with_options(archive:, dir:, options:) execute_cmd(%W(tar -#{options} #{archive} -C #{dir})) execute_cmd(%W(chmod -R #{UNTAR_MASK} #{dir})) - remove_symlinks(dir) + clean_extraction_dir!(dir) end # rubocop:disable Gitlab/ModuleWithInstanceVariables @@ -122,17 +125,27 @@ module Gitlab true end - def remove_symlinks(dir) - ignore_file_names = %w[. ..] - + # Scans and cleans the directory tree. + # Symlinks are considered legal but are removed. + # Files sharing hard links are considered illegal and the directory will be removed + # and a `HardLinkError` exception will be raised. + # + # @raise [HardLinkError] if there multiple hard links to the same file detected. + # @return [Boolean] true + def clean_extraction_dir!(dir) # Using File::FNM_DOTMATCH to also delete symlinks starting with "." - Dir.glob("#{dir}/**/*", File::FNM_DOTMATCH) - .reject { |f| ignore_file_names.include?(File.basename(f)) } - .each do |filepath| - FileUtils.rm(filepath) if File.lstat(filepath).symlink? - end + Dir.glob("#{dir}/**/*", File::FNM_DOTMATCH).each do |filepath| + next if CLEAN_DIR_IGNORE_FILE_NAMES.include?(File.basename(filepath)) + + raise HardLinkError, 'File shares hard link' if Gitlab::Utils::FileInfo.shares_hard_link?(filepath) + + FileUtils.rm(filepath) if Gitlab::Utils::FileInfo.linked?(filepath) + end true + rescue HardLinkError + FileUtils.remove_dir(dir) + raise end end end diff --git a/lib/gitlab/import_export/decompressed_archive_size_validator.rb b/lib/gitlab/import_export/decompressed_archive_size_validator.rb index 104c9e6c456..2e39f3f38c2 100644 --- a/lib/gitlab/import_export/decompressed_archive_size_validator.rb +++ b/lib/gitlab/import_export/decompressed_archive_size_validator.rb @@ -87,7 +87,7 @@ module Gitlab def validate_archive_path Gitlab::PathTraversal.check_path_traversal!(@archive_path) - raise(ServiceError, 'Archive path is a symlink') if File.lstat(@archive_path).symlink? + raise(ServiceError, 'Archive path is a symlink or hard link') if Gitlab::Utils::FileInfo.linked?(@archive_path) raise(ServiceError, 'Archive path is not a file') unless File.file?(@archive_path) end diff --git a/lib/gitlab/import_export/file_importer.rb b/lib/gitlab/import_export/file_importer.rb index d2593289c23..37c83e88ef2 100644 --- a/lib/gitlab/import_export/file_importer.rb +++ b/lib/gitlab/import_export/file_importer.rb @@ -23,7 +23,7 @@ module Gitlab mkdir_p(@shared.export_path) mkdir_p(@shared.archive_path) - remove_symlinks(@shared.export_path) + clean_extraction_dir!(@shared.export_path) copy_archive wait_for_archived_file do @@ -35,7 +35,7 @@ module Gitlab false ensure remove_import_file - remove_symlinks(@shared.export_path) + clean_extraction_dir!(@shared.export_path) end private diff --git a/lib/gitlab/import_export/json/ndjson_reader.rb b/lib/gitlab/import_export/json/ndjson_reader.rb index 3de56aacf18..93a94716f8d 100644 --- a/lib/gitlab/import_export/json/ndjson_reader.rb +++ b/lib/gitlab/import_export/json/ndjson_reader.rb @@ -21,7 +21,9 @@ module Gitlab # This reads from `tree/project.json` path = file_path("#{importable_path}.json") - raise Gitlab::ImportExport::Error, 'Invalid file' if !File.exist?(path) || File.symlink?(path) + if !File.exist?(path) || Gitlab::Utils::FileInfo.linked?(path) + raise Gitlab::ImportExport::Error, 'Invalid file' + end data = File.read(path, MAX_JSON_DOCUMENT_SIZE) json_decode(data) @@ -34,7 +36,7 @@ module Gitlab # This reads from `tree/project/merge_requests.ndjson` path = file_path(importable_path, "#{key}.ndjson") - next if !File.exist?(path) || File.symlink?(path) + next if !File.exist?(path) || Gitlab::Utils::FileInfo.linked?(path) File.foreach(path, MAX_JSON_DOCUMENT_SIZE).with_index do |line, line_num| documents << [json_decode(line), line_num] diff --git a/lib/gitlab/import_export/recursive_merge_folders.rb b/lib/gitlab/import_export/recursive_merge_folders.rb index 827385d4daf..e6eba60db93 100644 --- a/lib/gitlab/import_export/recursive_merge_folders.rb +++ b/lib/gitlab/import_export/recursive_merge_folders.rb @@ -57,7 +57,7 @@ module Gitlab source_child = File.join(source_path, child) target_child = File.join(target_path, child) - next if File.lstat(source_child).symlink? + next if Gitlab::Utils::FileInfo.linked?(source_child) if File.directory?(source_child) FileUtils.mkdir_p(target_child, mode: DEFAULT_DIR_MODE) unless File.exist?(target_child) diff --git a/lib/gitlab/pages/virtual_host_finder.rb b/lib/gitlab/pages/virtual_host_finder.rb index 5fec60188f8..d5e2159fb52 100644 --- a/lib/gitlab/pages/virtual_host_finder.rb +++ b/lib/gitlab/pages/virtual_host_finder.rb @@ -10,13 +10,12 @@ module Gitlab def execute return if host.blank? - gitlab_host = ::Settings.pages.host.downcase.prepend(".") + gitlab_host = ::Gitlab.config.pages.host.downcase.prepend(".") if host.ends_with?(gitlab_host) name = host.delete_suffix(gitlab_host) - by_namespace_domain(name) || - by_unique_domain(name) + by_unique_domain(name) || by_namespace_domain(name) else by_custom_domain(host) end diff --git a/lib/gitlab/utils/file_info.rb b/lib/gitlab/utils/file_info.rb new file mode 100644 index 00000000000..a0ec370e225 --- /dev/null +++ b/lib/gitlab/utils/file_info.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module Gitlab + module Utils + module FileInfo + class << self + # Returns true if: + # - File or directory is a symlink. + # - File shares a hard link. + def linked?(file) + stat = to_file_stat(file) + + stat.symlink? || shares_hard_link?(stat) + end + + # Returns: + # - true if file shares a hard link with another file. + # - false if file is a directory, as directories cannot be hard linked. + def shares_hard_link?(file) + stat = to_file_stat(file) + + stat.file? && stat.nlink > 1 + end + + private + + def to_file_stat(filepath_or_stat) + return filepath_or_stat if filepath_or_stat.is_a?(File::Stat) + + File.lstat(filepath_or_stat) + end + end + end + end +end -- cgit v1.2.3