1# frozen_string_literal: true 2 3# This middleware sets the SameSite directive to None on all cookies. 4# It also adds the Secure directive if HTTPS is enabled. 5# 6# Chrome v80, rolled out in March 2020, treats any cookies without the 7# SameSite directive set as though they are SameSite=Lax 8# (https://www.chromestatus.com/feature/5088147346030592). This is a 9# breaking change from the previous default behavior, which was to treat 10# those cookies as SameSite=None. 11# 12# This middleware is needed until we upgrade to Rack v2.1.0+ 13# (https://github.com/rack/rack/commit/c859bbf7b53cb59df1837612a8c330dfb4147392) 14# and a version of Rails that has native support 15# (https://github.com/rails/rails/commit/7ccaa125ba396d418aad1b217b63653d06044680). 16# 17module Gitlab 18 module Middleware 19 class SameSiteCookies 20 COOKIE_SEPARATOR = "\n" 21 22 def initialize(app) 23 @app = app 24 end 25 26 def call(env) 27 status, headers, body = @app.call(env) 28 result = [status, headers, body] 29 30 set_cookie = headers['Set-Cookie']&.strip 31 32 return result if set_cookie.blank? || !ssl? 33 return result if same_site_none_incompatible?(env['HTTP_USER_AGENT']) 34 35 cookies = set_cookie.split(COOKIE_SEPARATOR) 36 37 cookies.each do |cookie| 38 next if cookie.blank? 39 40 # Chrome will drop SameSite=None cookies without the Secure 41 # flag. If we remove this middleware, we may need to ensure 42 # that all cookies set this flag. 43 unless SECURE_REGEX.match?(cookie) 44 cookie << '; Secure' 45 end 46 47 unless SAME_SITE_REGEX.match?(cookie) 48 cookie << '; SameSite=None' 49 end 50 end 51 52 headers['Set-Cookie'] = cookies.join(COOKIE_SEPARATOR) 53 54 result 55 end 56 57 private 58 59 # Taken from https://www.chromium.org/updates/same-site/incompatible-clients 60 # We use RE2 instead of the browser gem for performance. 61 IOS_REGEX = RE2('\(iP.+; CPU .*OS (\d+)[_\d]*.*\) AppleWebKit\/') 62 MACOS_REGEX = RE2('\(Macintosh;.*Mac OS X (\d+)_(\d+)[_\d]*.*\) AppleWebKit\/') 63 SAFARI_REGEX = RE2('Version\/.* Safari\/') 64 CHROMIUM_REGEX = RE2('Chrom(e|ium)') 65 CHROMIUM_VERSION_REGEX = RE2('Chrom[^ \/]+\/(\d+)') 66 UC_BROWSER_REGEX = RE2('UCBrowser\/') 67 UC_BROWSER_VERSION_REGEX = RE2('UCBrowser\/(\d+)\.(\d+)\.(\d+)') 68 69 SECURE_REGEX = RE2(';\s*secure', case_sensitive: false) 70 SAME_SITE_REGEX = RE2(';\s*samesite=', case_sensitive: false) 71 72 def ssl? 73 Gitlab.config.gitlab.https 74 end 75 76 def same_site_none_incompatible?(user_agent) 77 return false if user_agent.blank? 78 79 has_webkit_same_site_bug?(user_agent) || drops_unrecognized_same_site_cookies?(user_agent) 80 end 81 82 def has_webkit_same_site_bug?(user_agent) 83 ios_version?(12, user_agent) || 84 (macos_version?(10, 14, user_agent) && safari?(user_agent)) 85 end 86 87 def drops_unrecognized_same_site_cookies?(user_agent) 88 if uc_browser?(user_agent) 89 return !uc_browser_version_at_least?(12, 13, 2, user_agent) 90 end 91 92 chromium_based?(user_agent) && chromium_version_between?(51, 66, user_agent) 93 end 94 95 def ios_version?(major, user_agent) 96 m = IOS_REGEX.match(user_agent) 97 98 return false if m.nil? 99 100 m[1].to_i == major 101 end 102 103 def macos_version?(major, minor, user_agent) 104 m = MACOS_REGEX.match(user_agent) 105 106 return false if m.nil? 107 108 m[1].to_i == major && m[2].to_i == minor 109 end 110 111 def safari?(user_agent) 112 SAFARI_REGEX.match?(user_agent) 113 end 114 115 def chromium_based?(user_agent) 116 CHROMIUM_REGEX.match?(user_agent) 117 end 118 119 def chromium_version_between?(from_major, to_major, user_agent) 120 m = CHROMIUM_VERSION_REGEX.match(user_agent) 121 122 return false if m.nil? 123 124 version = m[1].to_i 125 version >= from_major && version <= to_major 126 end 127 128 def uc_browser?(user_agent) 129 UC_BROWSER_REGEX.match?(user_agent) 130 end 131 132 def uc_browser_version_at_least?(major, minor, build, user_agent) 133 m = UC_BROWSER_VERSION_REGEX.match(user_agent) 134 135 return false if m.nil? 136 137 major_version = m[1].to_i 138 minor_version = m[2].to_i 139 build_version = m[3].to_i 140 141 return major_version > major if major_version != major 142 return minor_version > minor if minor_version != minor 143 144 build_version >= build 145 end 146 end 147 end 148end 149