149 lines
		
	
	
		
			4.4 KiB
		
	
	
	
		
			Ruby
		
	
	
	
			
		
		
	
	
			149 lines
		
	
	
		
			4.4 KiB
		
	
	
	
		
			Ruby
		
	
	
	
# frozen_string_literal: true
 | 
						|
 | 
						|
# This middleware sets the SameSite directive to None on all cookies.
 | 
						|
# It also adds the Secure directive if HTTPS is enabled.
 | 
						|
#
 | 
						|
# Chrome v80, rolled out in March 2020, treats any cookies without the
 | 
						|
# SameSite directive set as though they are SameSite=Lax
 | 
						|
# (https://www.chromestatus.com/feature/5088147346030592). This is a
 | 
						|
# breaking change from the previous default behavior, which was to treat
 | 
						|
# those cookies as SameSite=None.
 | 
						|
#
 | 
						|
# This middleware is needed until we upgrade to Rack v2.1.0+
 | 
						|
# (https://github.com/rack/rack/commit/c859bbf7b53cb59df1837612a8c330dfb4147392)
 | 
						|
# and a version of Rails that has native support
 | 
						|
# (https://github.com/rails/rails/commit/7ccaa125ba396d418aad1b217b63653d06044680).
 | 
						|
#
 | 
						|
module Gitlab
 | 
						|
  module Middleware
 | 
						|
    class SameSiteCookies
 | 
						|
      COOKIE_SEPARATOR = "\n"
 | 
						|
 | 
						|
      def initialize(app)
 | 
						|
        @app = app
 | 
						|
      end
 | 
						|
 | 
						|
      def call(env)
 | 
						|
        status, headers, body = @app.call(env)
 | 
						|
        result = [status, headers, body]
 | 
						|
 | 
						|
        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)
 | 
						|
 | 
						|
        cookies.each do |cookie|
 | 
						|
          next if cookie.blank?
 | 
						|
 | 
						|
          # 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.
 | 
						|
          unless SECURE_REGEX.match?(cookie)
 | 
						|
            cookie << '; Secure'
 | 
						|
          end
 | 
						|
 | 
						|
          unless SAME_SITE_REGEX.match?(cookie)
 | 
						|
            cookie << '; SameSite=None'
 | 
						|
          end
 | 
						|
        end
 | 
						|
 | 
						|
        headers['Set-Cookie'] = cookies.join(COOKIE_SEPARATOR)
 | 
						|
 | 
						|
        result
 | 
						|
      end
 | 
						|
 | 
						|
      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
 |