gitlab-org--gitlab-foss/lib/gitlab/middleware/same_site_cookies.rb

148 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".freeze
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