diff options
Diffstat (limited to 'lib/gitlab/middleware')
-rw-r--r-- | lib/gitlab/middleware/multipart.rb | 99 | ||||
-rw-r--r-- | lib/gitlab/middleware/read_only/controller.rb | 2 | ||||
-rw-r--r-- | lib/gitlab/middleware/same_site_cookies.rb | 89 |
3 files changed, 181 insertions, 9 deletions
diff --git a/lib/gitlab/middleware/multipart.rb b/lib/gitlab/middleware/multipart.rb index c0b671abd44..8e6ac7610f2 100644 --- a/lib/gitlab/middleware/multipart.rb +++ b/lib/gitlab/middleware/multipart.rb @@ -29,6 +29,8 @@ module Gitlab module Middleware class Multipart RACK_ENV_KEY = 'HTTP_GITLAB_WORKHORSE_MULTIPART_FIELDS' + JWT_PARAM_SUFFIX = '.gitlab-workhorse-upload' + JWT_PARAM_FIXED_KEY = 'upload' class Handler def initialize(env, message) @@ -57,7 +59,8 @@ module Gitlab yield ensure - @open_files.each(&:close) + @open_files.compact + .each(&:close) end # This function calls itself recursively @@ -122,15 +125,89 @@ module Gitlab def allowed_paths [ + Dir.tmpdir, ::FileUploader.root, - Gitlab.config.uploads.storage_path, - JobArtifactUploader.workhorse_upload_path, - LfsObjectUploader.workhorse_upload_path, + ::Gitlab.config.uploads.storage_path, + ::JobArtifactUploader.workhorse_upload_path, + ::LfsObjectUploader.workhorse_upload_path, File.join(Rails.root, 'public/uploads/tmp') ] + package_allowed_paths end end + # TODO this class is meant to replace Handler when the feature flag + # upload_middleware_jwt_params_handler is removed + class HandlerForJWTParams < Handler + def with_open_files + @rewritten_fields.keys.each do |field| + parsed_field = Rack::Utils.parse_nested_query(field) + raise "unexpected field: #{field.inspect}" unless parsed_field.count == 1 + + key, value = parsed_field.first + if value.nil? # we have a top level param, eg. field = 'foo' and not 'foo[bar]' + raise "invalid field: #{field.inspect}" if field != key + + value = open_file(extract_upload_params_from(@request.params, with_prefix: key)) + @open_files << value + else + value = decorate_params_value(value, @request.params[key]) + end + + update_param(key, value) + end + + yield + ensure + @open_files.compact + .each(&:close) + end + + # This function calls itself recursively + def decorate_params_value(hash_path, value_hash) + unless hash_path.is_a?(Hash) && hash_path.count == 1 + raise "invalid path: #{hash_path.inspect}" + end + + path_key, path_value = hash_path.first + + unless value_hash.is_a?(Hash) && value_hash[path_key] + raise "invalid value hash: #{value_hash.inspect}" + end + + case path_value + when nil + value_hash[path_key] = open_file(extract_upload_params_from(value_hash[path_key])) + @open_files << value_hash[path_key] + value_hash + when Hash + decorate_params_value(path_value, value_hash[path_key]) + value_hash + else + raise "unexpected path value: #{path_value.inspect}" + end + end + + def open_file(params) + ::UploadedFile.from_params_without_field(params, allowed_paths) + end + + private + + def extract_upload_params_from(params, with_prefix: '') + param_key = "#{with_prefix}#{JWT_PARAM_SUFFIX}" + jwt_token = params[param_key] + raise "Empty JWT param: #{param_key}" if jwt_token.blank? + + payload = Gitlab::Workhorse.decode_jwt(jwt_token).first + raise "Invalid JWT payload: not a Hash" unless payload.is_a?(Hash) + + upload_params = payload.fetch(JWT_PARAM_FIXED_KEY, {}) + raise "Empty params for: #{param_key}" if upload_params.empty? + + upload_params + end + end + def initialize(app) @app = app end @@ -139,14 +216,24 @@ module Gitlab encoded_message = env.delete(RACK_ENV_KEY) return @app.call(env) if encoded_message.blank? - message = Gitlab::Workhorse.decode_jwt(encoded_message)[0] + message = ::Gitlab::Workhorse.decode_jwt(encoded_message)[0] - Handler.new(env, message).with_open_files do + handler_class.new(env, message).with_open_files do @app.call(env) end rescue UploadedFile::InvalidPathError => e [400, { 'Content-Type' => 'text/plain' }, e.message] end + + private + + def handler_class + if Feature.enabled?(:upload_middleware_jwt_params_handler) + ::Gitlab::Middleware::Multipart::HandlerForJWTParams + else + ::Gitlab::Middleware::Multipart::Handler + end + end end end end diff --git a/lib/gitlab/middleware/read_only/controller.rb b/lib/gitlab/middleware/read_only/controller.rb index 6573506e926..cfea4aaddf3 100644 --- a/lib/gitlab/middleware/read_only/controller.rb +++ b/lib/gitlab/middleware/read_only/controller.rb @@ -36,7 +36,7 @@ module Gitlab def call if disallowed_request? && Gitlab::Database.read_only? - Rails.logger.debug('GitLab ReadOnly: preventing possible non read-only operation') # rubocop:disable Gitlab/RailsLogger + Gitlab::AppLogger.debug('GitLab ReadOnly: preventing possible non read-only operation') if json_request? return [403, { 'Content-Type' => APPLICATION_JSON }, [{ 'message' => ERROR_MESSAGE }.to_json]] diff --git a/lib/gitlab/middleware/same_site_cookies.rb b/lib/gitlab/middleware/same_site_cookies.rb index 45968035e79..37ccc5abb10 100644 --- a/lib/gitlab/middleware/same_site_cookies.rb +++ b/lib/gitlab/middleware/same_site_cookies.rb @@ -30,6 +30,7 @@ module Gitlab set_cookie = headers['Set-Cookie']&.strip return result if set_cookie.blank? || !ssl? + return result if same_site_none_incompatible?(env['HTTP_USER_AGENT']) cookies = set_cookie.split(COOKIE_SEPARATOR) @@ -39,11 +40,11 @@ module Gitlab # Chrome will drop SameSite=None cookies without the Secure # flag. If we remove this middleware, we may need to ensure # that all cookies set this flag. - if ssl? && !(cookie =~ /;\s*secure/i) + unless SECURE_REGEX.match?(cookie) cookie << '; Secure' end - unless cookie =~ /;\s*samesite=/i + unless SAME_SITE_REGEX.match?(cookie) cookie << '; SameSite=None' end end @@ -55,9 +56,93 @@ module Gitlab private + # Taken from https://www.chromium.org/updates/same-site/incompatible-clients + # We use RE2 instead of the browser gem for performance. + IOS_REGEX = RE2('\(iP.+; CPU .*OS (\d+)[_\d]*.*\) AppleWebKit\/') + MACOS_REGEX = RE2('\(Macintosh;.*Mac OS X (\d+)_(\d+)[_\d]*.*\) AppleWebKit\/') + SAFARI_REGEX = RE2('Version\/.* Safari\/') + CHROMIUM_REGEX = RE2('Chrom(e|ium)') + CHROMIUM_VERSION_REGEX = RE2('Chrom[^ \/]+\/(\d+)') + UC_BROWSER_REGEX = RE2('UCBrowser\/') + UC_BROWSER_VERSION_REGEX = RE2('UCBrowser\/(\d+)\.(\d+)\.(\d+)') + + SECURE_REGEX = RE2(';\s*secure', case_sensitive: false) + SAME_SITE_REGEX = RE2(';\s*samesite=', case_sensitive: false) + def ssl? Gitlab.config.gitlab.https end + + def same_site_none_incompatible?(user_agent) + return false if user_agent.blank? + + has_webkit_same_site_bug?(user_agent) || drops_unrecognized_same_site_cookies?(user_agent) + end + + def has_webkit_same_site_bug?(user_agent) + ios_version?(12, user_agent) || + (macos_version?(10, 14, user_agent) && safari?(user_agent)) + end + + def drops_unrecognized_same_site_cookies?(user_agent) + if uc_browser?(user_agent) + return !uc_browser_version_at_least?(12, 13, 2, user_agent) + end + + chromium_based?(user_agent) && chromium_version_between?(51, 66, user_agent) + end + + def ios_version?(major, user_agent) + m = IOS_REGEX.match(user_agent) + + return false if m.nil? + + m[1].to_i == major + end + + def macos_version?(major, minor, user_agent) + m = MACOS_REGEX.match(user_agent) + + return false if m.nil? + + m[1].to_i == major && m[2].to_i == minor + end + + def safari?(user_agent) + SAFARI_REGEX.match?(user_agent) + end + + def chromium_based?(user_agent) + CHROMIUM_REGEX.match?(user_agent) + end + + def chromium_version_between?(from_major, to_major, user_agent) + m = CHROMIUM_VERSION_REGEX.match(user_agent) + + return false if m.nil? + + version = m[1].to_i + version >= from_major && version <= to_major + end + + def uc_browser?(user_agent) + UC_BROWSER_REGEX.match?(user_agent) + end + + def uc_browser_version_at_least?(major, minor, build, user_agent) + m = UC_BROWSER_VERSION_REGEX.match(user_agent) + + return false if m.nil? + + major_version = m[1].to_i + minor_version = m[2].to_i + build_version = m[3].to_i + + return major_version > major if major_version != major + return minor_version > minor if minor_version != minor + + build_version >= build + end end end end |