diff options
Diffstat (limited to 'lib/bulk_imports')
-rw-r--r-- | lib/bulk_imports/file_downloads/filename_fetch.rb | 46 | ||||
-rw-r--r-- | lib/bulk_imports/file_downloads/validations.rb | 58 |
2 files changed, 104 insertions, 0 deletions
diff --git a/lib/bulk_imports/file_downloads/filename_fetch.rb b/lib/bulk_imports/file_downloads/filename_fetch.rb new file mode 100644 index 00000000000..b6bb0fd8c81 --- /dev/null +++ b/lib/bulk_imports/file_downloads/filename_fetch.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +module BulkImports + module FileDownloads + module FilenameFetch + REMOTE_FILENAME_PATTERN = %r{filename="(?<filename>[^"]+)"}.freeze + FILENAME_SIZE_LIMIT = 255 # chars before the extension + + def raise_error(message) + raise NotImplementedError + end + + private + + # Fetch the remote filename information from the request content-disposition header + # - Raises if the filename does not exist + # - If the filename is longer then 255 chars truncate it + # to be a total of 255 chars (with the extension) + # rubocop:disable Gitlab/ModuleWithInstanceVariables + def remote_filename + @remote_filename ||= begin + pattern = BulkImports::FileDownloads::FilenameFetch::REMOTE_FILENAME_PATTERN + name = response_headers['content-disposition'].to_s + .match(pattern) # matches the filename pattern + .then { |match| match&.named_captures || {} } # ensures the match is a hash + .fetch('filename') # fetches the 'filename' key or raise KeyError + + name = File.basename(name) # Ensures to remove path from the filename (../ for instance) + ensure_filename_size(name) # Ensures the filename is within the FILENAME_SIZE_LIMIT + end + rescue KeyError + raise_error 'Remote filename not provided in content-disposition header' + end + # rubocop:enable Gitlab/ModuleWithInstanceVariables + + def ensure_filename_size(filename) + limit = BulkImports::FileDownloads::FilenameFetch::FILENAME_SIZE_LIMIT + return filename if filename.length <= limit + + extname = File.extname(filename) + basename = File.basename(filename, extname)[0, limit] + "#{basename}#{extname}" + end + end + end +end diff --git a/lib/bulk_imports/file_downloads/validations.rb b/lib/bulk_imports/file_downloads/validations.rb new file mode 100644 index 00000000000..ae94267a6e8 --- /dev/null +++ b/lib/bulk_imports/file_downloads/validations.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +module BulkImports + module FileDownloads + module Validations + def raise_error(message) + raise NotImplementedError + end + + def filepath + raise NotImplementedError + end + + def file_size_limit + raise NotImplementedError + end + + def response_headers + raise NotImplementedError + end + + private + + def validate_filepath + Gitlab::Utils.check_path_traversal!(filepath) + end + + def validate_content_type + content_type = response_headers['content-type'] + + raise_error('Invalid content type') if content_type.blank? || allowed_content_types.exclude?(content_type) + end + + def validate_symlink + return unless File.lstat(filepath).symlink? + + File.delete(filepath) + raise_error 'Invalid downloaded file' + end + + def validate_content_length + validate_size!(response_headers['content-length']) + end + + def validate_size!(size) + if size.blank? + raise_error 'Missing content-length header' + elsif size.to_i > file_size_limit + raise_error format( + "File size %{size} exceeds limit of %{limit}", + size: ActiveSupport::NumberHelper.number_to_human_size(size), + limit: ActiveSupport::NumberHelper.number_to_human_size(file_size_limit) + ) + end + end + end + end +end |