2020-03-28 05:08:30 -04:00
|
|
|
# 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?
|
2020-09-01 08:11:01 -04:00
|
|
|
return result if same_site_none_incompatible?(env['HTTP_USER_AGENT'])
|
2020-03-28 05:08:30 -04:00
|
|
|
|
|
|
|
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.
|
2020-08-30 20:10:37 -04:00
|
|
|
unless SECURE_REGEX.match?(cookie)
|
2020-03-28 05:08:30 -04:00
|
|
|
cookie << '; Secure'
|
|
|
|
end
|
|
|
|
|
2020-08-30 20:10:37 -04:00
|
|
|
unless SAME_SITE_REGEX.match?(cookie)
|
2020-03-28 05:08:30 -04:00
|
|
|
cookie << '; SameSite=None'
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
headers['Set-Cookie'] = cookies.join(COOKIE_SEPARATOR)
|
|
|
|
|
|
|
|
result
|
|
|
|
end
|
|
|
|
|
|
|
|
private
|
|
|
|
|
2020-08-30 20:10:37 -04:00
|
|
|
# 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)
|
|
|
|
|
2020-03-28 05:08:30 -04:00
|
|
|
def ssl?
|
|
|
|
Gitlab.config.gitlab.https
|
|
|
|
end
|
2020-08-30 20:10:37 -04:00
|
|
|
|
|
|
|
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
|
2020-03-28 05:08:30 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|