diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-06-03 00:59:19 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-06-03 00:59:19 +0300 |
commit | 1385478346704d03ab9d3a9bf8ae3812cea0b6b5 (patch) | |
tree | c2b68728119200c48fbfe09bb09397d4e31659b7 /lib/gitlab/ci | |
parent | 361d9dae8bafae8c830d68d16ea0f76482ba9343 (diff) |
Add latest changes from gitlab-org/security/gitlab@16-0-stable-ee
Diffstat (limited to 'lib/gitlab/ci')
-rw-r--r-- | lib/gitlab/ci/artifacts/decompressed_artifact_size_validator.rb | 60 | ||||
-rw-r--r-- | lib/gitlab/ci/decompressed_gzip_size_validator.rb | 79 |
2 files changed, 139 insertions, 0 deletions
diff --git a/lib/gitlab/ci/artifacts/decompressed_artifact_size_validator.rb b/lib/gitlab/ci/artifacts/decompressed_artifact_size_validator.rb new file mode 100644 index 00000000000..04930da5e30 --- /dev/null +++ b/lib/gitlab/ci/artifacts/decompressed_artifact_size_validator.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Artifacts + class DecompressedArtifactSizeValidator + DEFAULT_MAX_BYTES = 4.gigabytes.freeze + + FILE_FORMAT_VALIDATORS = { + gzip: ::Gitlab::Ci::DecompressedGzipSizeValidator + }.freeze + + FileDecompressionError = Class.new(StandardError) + + def initialize(file:, file_format:, max_bytes: DEFAULT_MAX_BYTES) + @file = file + @file_path = file&.path + @file_format = file_format + @max_bytes = max_bytes + end + + def validate! + validator_class = FILE_FORMAT_VALIDATORS[file_format.to_sym] + + return if file_path.nil? + return if validator_class.nil? + + if file.respond_to?(:object_store) && file.object_store == ObjectStorage::Store::REMOTE + return if valid_on_storage?(validator_class) + elsif validator_class.new(archive_path: file_path, max_bytes: max_bytes).valid? + return + end + + raise(FileDecompressionError, 'File decompression error') + end + + private + + attr_reader :file_path, :file, :file_format, :max_bytes + + def valid_on_storage?(validator_class) + temp_filename = "#{SecureRandom.uuid}.gz" + + is_valid = false + Tempfile.open(temp_filename, '/tmp') do |tempfile| + tempfile.binmode + ::Faraday.get(file.url) do |req| + req.options.on_data = proc { |chunk, _| tempfile.write(chunk) } + end + + is_valid = validator_class.new(archive_path: tempfile.path, max_bytes: max_bytes).valid? + tempfile.unlink + end + + is_valid + end + end + end + end +end diff --git a/lib/gitlab/ci/decompressed_gzip_size_validator.rb b/lib/gitlab/ci/decompressed_gzip_size_validator.rb new file mode 100644 index 00000000000..a92f3007671 --- /dev/null +++ b/lib/gitlab/ci/decompressed_gzip_size_validator.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + class DecompressedGzipSizeValidator + DEFAULT_MAX_BYTES = 4.gigabytes.freeze + TIMEOUT_LIMIT = 210.seconds + + ServiceError = Class.new(StandardError) + + def initialize(archive_path:, max_bytes: DEFAULT_MAX_BYTES) + @archive_path = archive_path + @max_bytes = max_bytes + end + + def valid? + validate + end + + private + + def validate + pgrps = nil + valid_archive = true + + validate_archive_path + + Timeout.timeout(TIMEOUT_LIMIT) do + stderr_r, stderr_w = IO.pipe + stdout, wait_threads = Open3.pipeline_r(*command, pgroup: true, err: stderr_w) + + # When validation is performed on a small archive (e.g. 100 bytes) + # `wait_thr` finishes before we can get process group id. Do not + # raise exception in this scenario. + pgrps = wait_threads.map do |wait_thr| + Process.getpgid(wait_thr[:pid]) + rescue Errno::ESRCH + nil + end + pgrps.compact! + + status = wait_threads.last.value + + if status.success? + result = stdout.readline + + valid_archive = false if result.to_i > max_bytes + else + valid_archive = false + end + + ensure + stdout.close + stderr_w.close + stderr_r.close + end + + valid_archive + rescue StandardError + pgrps.each { |pgrp| Process.kill(-1, pgrp) } if pgrps + + false + end + + def validate_archive_path + Gitlab::Utils.check_path_traversal!(archive_path) + + raise(ServiceError, 'Archive path is a symlink') if File.lstat(archive_path).symlink? + raise(ServiceError, 'Archive path is not a file') unless File.file?(archive_path) + end + + def command + [['gzip', '-dc', archive_path], ['wc', '-c']] + end + + attr_reader :archive_path, :max_bytes + end + end +end |