From 97ea390dacea2cdda94ec6143f37a844255353f3 Mon Sep 17 00:00:00 2001 From: pmaytak <34331512+pmaytak@users.noreply.github.com> Date: Mon, 29 Jun 2020 23:35:19 -0700 Subject: [PATCH] Updated SameSite compatibility check. (#262) --- .../CookiePolicyOptionsExtensions.cs | 138 +++++++++++++----- .../Microsoft.Identity.Web.xml | 20 ++- .../CookiePolicyOptionsExtensionsTests.cs | 9 +- 3 files changed, 127 insertions(+), 40 deletions(-) diff --git a/src/Microsoft.Identity.Web/CookiePolicyOptionsExtensions.cs b/src/Microsoft.Identity.Web/CookiePolicyOptionsExtensions.cs index 4862eac0d..37e405357 100644 --- a/src/Microsoft.Identity.Web/CookiePolicyOptionsExtensions.cs +++ b/src/Microsoft.Identity.Web/CookiePolicyOptionsExtensions.cs @@ -2,6 +2,8 @@ // Licensed under the MIT License. using System; +using System.Globalization; +using System.Text.RegularExpressions; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; @@ -14,7 +16,7 @@ public static class CookiePolicyOptionsExtensions { /// /// Handles SameSite cookie issue according to the https://docs.microsoft.com/en-us/aspnet/core/security/samesite?view=aspnetcore-3.1. - /// The default list of user-agents that disallow SameSite None, was taken from https://devblogs.microsoft.com/aspnet/upcoming-samesite-cookie-changes-in-asp-net-and-asp-net-core/. + /// The default list of user agents that disallow "SameSite=None", was taken from https://devblogs.microsoft.com/aspnet/upcoming-samesite-cookie-changes-in-asp-net-and-asp-net-core/. /// /// to update. /// to chain. @@ -25,10 +27,10 @@ public static CookiePolicyOptions HandleSameSiteCookieCompatibility(this CookieP /// /// Handles SameSite cookie issue according to the docs: https://docs.microsoft.com/en-us/aspnet/core/security/samesite?view=aspnetcore-3.1 - /// The default list of user-agents that disallow SameSite None, was taken from https://devblogs.microsoft.com/aspnet/upcoming-samesite-cookie-changes-in-asp-net-and-asp-net-core/. + /// The default list of user agents that disallow "SameSite=None", was taken from https://devblogs.microsoft.com/aspnet/upcoming-samesite-cookie-changes-in-asp-net-and-asp-net-core/. /// /// to update. - /// If you don't want to use the default user-agent list implementation, the method sent in this parameter will be run against the user-agent and if returned true, SameSite value will be set to Unspecified. The default user-agent list used can be found at: https://devblogs.microsoft.com/aspnet/upcoming-samesite-cookie-changes-in-asp-net-and-asp-net-core/. + /// If you don't want to use the default user agent list implementation, the method sent in this parameter will be run against the user agent and if returned true, SameSite value will be set to Unspecified. The default user agent list used can be found at: https://devblogs.microsoft.com/aspnet/upcoming-samesite-cookie-changes-in-asp-net-and-asp-net-core/. /// to chain. public static CookiePolicyOptions HandleSameSiteCookieCompatibility(this CookiePolicyOptions options, Func disallowsSameSiteNone) { @@ -59,50 +61,120 @@ private static void CheckSameSite(HttpContext httpContext, CookieOptions options } /// - /// Checks if the specified user agent supports SameSite None cookies. + /// Checks if the specified user agent supports "SameSite=None" cookies. /// /// Browser user agent. - /// Method taken from https://devblogs.microsoft.com/aspnet/upcoming-samesite-cookie-changes-in-asp-net-and-asp-net-core/. - /// True, if the user agent does not allow SameSite None cookie; otherwise, false. + /// + /// Incompatible user agents include: + /// + /// Versions of Chrome from Chrome 51 to Chrome 66 (inclusive on both ends). + /// Versions of UC Browser on Android prior to version 12.13.2. + /// Versions of Safari and embedded browsers on MacOS 10.14 and all browsers on iOS 12. + /// + /// Reference: https://www.chromium.org/updates/same-site/incompatible-clients. + /// + /// True, if the user agent does not allow "SameSite=None" cookie; otherwise, false. public static bool DisallowsSameSiteNone(string userAgent) { - if (!string.IsNullOrEmpty(userAgent)) + return HasWebKitSameSiteBug() || + DropsUnrecognizedSameSiteCookies(); + + bool HasWebKitSameSiteBug() => + IsIosVersion(12) || + (IsMacosxVersion(10, 14) && + (IsSafari() || IsMacEmbeddedBrowser())); + + bool DropsUnrecognizedSameSiteCookies() { - // Cover all iOS based browsers here. This includes: - // - Safari on iOS 12 for iPhone, iPod Touch, iPad - // - WkWebview on iOS 12 for iPhone, iPod Touch, iPad - // - Chrome on iOS 12 for iPhone, iPod Touch, iPad - // All of which are broken by SameSite=None, because they use the iOS networking - // stack. - if (userAgent.Contains("CPU iPhone OS 12") || - userAgent.Contains("iPad; CPU OS 12")) + if (IsUcBrowser()) { - return true; + return !IsUcBrowserVersionAtLeast(12, 13, 2); } - // Cover Mac OS X based browsers that use the Mac OS networking stack. - // This includes: - // - Safari on Mac OS X. - // This does not include: - // - Chrome on Mac OS X - // Because they do not use the Mac OS networking stack. - if (userAgent.Contains("Macintosh; Intel Mac OS X 10_14") && - userAgent.Contains("Version/") && userAgent.Contains("Safari")) + return IsChromiumBased() && + IsChromiumVersionAtLeast(51) && + !IsChromiumVersionAtLeast(67); + } + + bool IsIosVersion(int major) + { + string regex = @"\(iP.+; CPU .*OS (\d+)[_\d]*.*\) AppleWebKit\/"; + + // Extract digits from first capturing group. + Match match = Regex.Match(userAgent, regex); + return match.Groups[1].Value == major.ToString(CultureInfo.CurrentCulture); + } + + bool IsMacosxVersion(int major, int minor) + { + string regex = @"\(Macintosh;.*Mac OS X (\d+)_(\d+)[_\d]*.*\) AppleWebKit\/"; + + // Extract digits from first and second capturing groups. + Match match = Regex.Match(userAgent, regex); + return match.Groups[1].Value == major.ToString(CultureInfo.CurrentCulture) && + match.Groups[2].Value == minor.ToString(CultureInfo.CurrentCulture); + } + + bool IsSafari() + { + string regex = @"Version\/.* Safari\/"; + + return Regex.IsMatch(userAgent, regex) && + !IsChromiumBased(); + } + + bool IsMacEmbeddedBrowser() + { + string regex = @"^Mozilla\/[\.\d]+ \(Macintosh;.*Mac OS X [_\d]+\) AppleWebKit\/[\.\d]+ \(KHTML, like Gecko\)$"; + + return Regex.IsMatch(userAgent, regex); + } + + bool IsChromiumBased() + { + string regex = "Chrom(e|ium)"; + + return Regex.IsMatch(userAgent, regex); + } + + bool IsChromiumVersionAtLeast(int major) + { + string regex = @"Chrom[^ \/]+\/(\d+)[\.\d]* "; + + // Extract digits from first capturing group. + Match match = Regex.Match(userAgent, regex); + int version = Convert.ToInt32(match.Groups[1].Value, CultureInfo.CurrentCulture); + return version >= major; + } + + bool IsUcBrowser() + { + string regex = @"UCBrowser\/"; + + return Regex.IsMatch(userAgent, regex); + } + + bool IsUcBrowserVersionAtLeast(int major, int minor, int build) + { + string regex = @"UCBrowser\/(\d+)\.(\d+)\.(\d+)[\.\d]* "; + + // Extract digits from three capturing groups. + Match match = Regex.Match(userAgent, regex); + int major_version = Convert.ToInt32(match.Groups[1].Value, CultureInfo.CurrentCulture); + int minor_version = Convert.ToInt32(match.Groups[2].Value, CultureInfo.CurrentCulture); + int build_version = Convert.ToInt32(match.Groups[3].Value, CultureInfo.CurrentCulture); + if (major_version != major) { - return true; + return major_version > major; } - // Cover Chrome 50-69, because some versions are broken by SameSite=None, - // and none in this range require it. - // Note: this covers some pre-Chromium Edge versions, - // but pre-Chromium Edge does not require SameSite=None. - if (userAgent.Contains("Chrome/5") || userAgent.Contains("Chrome/6")) + if (minor_version != minor) { - return true; + return minor_version > minor; } - } - return false; + return build_version >= build; + } } } } diff --git a/src/Microsoft.Identity.Web/Microsoft.Identity.Web.xml b/src/Microsoft.Identity.Web/Microsoft.Identity.Web.xml index 4de1cabba..aaf725a29 100644 --- a/src/Microsoft.Identity.Web/Microsoft.Identity.Web.xml +++ b/src/Microsoft.Identity.Web/Microsoft.Identity.Web.xml @@ -477,7 +477,7 @@ Handles SameSite cookie issue according to the https://docs.microsoft.com/en-us/aspnet/core/security/samesite?view=aspnetcore-3.1. - The default list of user-agents that disallow SameSite None, was taken from https://devblogs.microsoft.com/aspnet/upcoming-samesite-cookie-changes-in-asp-net-and-asp-net-core/. + The default list of user agents that disallow "SameSite=None", was taken from https://devblogs.microsoft.com/aspnet/upcoming-samesite-cookie-changes-in-asp-net-and-asp-net-core/. to update. to chain. @@ -485,19 +485,27 @@ Handles SameSite cookie issue according to the docs: https://docs.microsoft.com/en-us/aspnet/core/security/samesite?view=aspnetcore-3.1 - The default list of user-agents that disallow SameSite None, was taken from https://devblogs.microsoft.com/aspnet/upcoming-samesite-cookie-changes-in-asp-net-and-asp-net-core/. + The default list of user agents that disallow "SameSite=None", was taken from https://devblogs.microsoft.com/aspnet/upcoming-samesite-cookie-changes-in-asp-net-and-asp-net-core/. to update. - If you don't want to use the default user-agent list implementation, the method sent in this parameter will be run against the user-agent and if returned true, SameSite value will be set to Unspecified. The default user-agent list used can be found at: https://devblogs.microsoft.com/aspnet/upcoming-samesite-cookie-changes-in-asp-net-and-asp-net-core/. + If you don't want to use the default user agent list implementation, the method sent in this parameter will be run against the user agent and if returned true, SameSite value will be set to Unspecified. The default user agent list used can be found at: https://devblogs.microsoft.com/aspnet/upcoming-samesite-cookie-changes-in-asp-net-and-asp-net-core/. to chain. - Checks if the specified user agent supports SameSite None cookies. + Checks if the specified user agent supports "SameSite=None" cookies. Browser user agent. - Method taken from https://devblogs.microsoft.com/aspnet/upcoming-samesite-cookie-changes-in-asp-net-and-asp-net-core/. - True, if the user agent does not allow SameSite None cookie; otherwise, false. + + Incompatible user agents include: + + Versions of Chrome from Chrome 51 to Chrome 66 (inclusive on both ends). + Versions of UC Browser on Android prior to version 12.13.2. + Versions of Safari and embedded browsers on MacOS 10.14 and all browsers on iOS 12. + + Reference: https://www.chromium.org/updates/same-site/incompatible-clients. + + True, if the user agent does not allow "SameSite=None" cookie; otherwise, false. diff --git a/tests/Microsoft.Identity.Web.Test/CookiePolicyOptionsExtensionsTests.cs b/tests/Microsoft.Identity.Web.Test/CookiePolicyOptionsExtensionsTests.cs index d9c099af2..e81083283 100644 --- a/tests/Microsoft.Identity.Web.Test/CookiePolicyOptionsExtensionsTests.cs +++ b/tests/Microsoft.Identity.Web.Test/CookiePolicyOptionsExtensionsTests.cs @@ -97,6 +97,9 @@ public void HandleSameSiteCookieCompatibility_CustomFilter_ExecutesSuccessfully( [InlineData(true, "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Safari/605.1.15")] [InlineData(true, "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36")] [InlineData(true, "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36")] + [InlineData(true, "Mozilla/5.0 (Linux; U; Android 6.0.1; zh-CN; F5121 Build/34.0.A.1.247) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/40.0.2214.89 UCBrowser/11.5.1.944 Mobile Safari/537.36")] + [InlineData(true, "Mozilla/5.0 (Linux; U; Android 6.0; zh-CN; Redmi Note 4 Build/MRA58K) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/57.0.2987.108 UCBrowser/12.8.2.1062 Mobile Safari/537.36")] + [InlineData(true, "Mozilla/5.0 (Linux; U; Android 6.0; en-US; CAM-UL00 Build/HONORCAM-UL00) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/57.0.2987.108 UCBrowser/12.13.1.1189 Mobile Safari/537.36")] [InlineData(false, "Mozilla/5.0 (iPhone; CPU iPhone OS 13_1_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.1 Mobile/15E148 Safari/604.1")] [InlineData(false, "Mozilla/5.0 (iPhone; CPU iPhone OS 11_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/11.0 Mobile/15E148 Safari/604.1")] [InlineData(false, "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36")] @@ -105,6 +108,10 @@ public void HandleSameSiteCookieCompatibility_CustomFilter_ExecutesSuccessfully( [InlineData(false, "Mozilla/5.0 (iPad; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1")] [InlineData(false, "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1")] [InlineData(false, "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.90 Safari/537.36")] + [InlineData(false, "Mozilla/5.0 (Linux; U; Android 7.0; en-US; Redmi Note 4 Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/57.0.2987.108 UCBrowser/13.1.2.1293 Mobile Safari/537.36")] + [InlineData(false, "Mozilla/5.0 (Linux; U; Android 6.0; en-US; CAM-UL00 Build/HONORCAM-UL00) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/57.0.2987.108 UCBrowser/12.14.5.1189 Mobile Safari/537.36")] + [InlineData(false, "Mozilla/5.0 (Linux; U; Android 6.0; en-US; CAM-UL00 Build/HONORCAM-UL00) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/57.0.2987.108 UCBrowser/12.13.3.1189 Mobile Safari/537.36")] + [InlineData(false, "Invalid user agent")] public void DisallowsSameSiteNone_VariousUserAgents_ExecutesSuccessfully(bool expectedResult, string userAgent) { var actualResult = CookiePolicyOptionsExtensions.DisallowsSameSiteNone(userAgent); @@ -112,4 +119,4 @@ public void DisallowsSameSiteNone_VariousUserAgents_ExecutesSuccessfully(bool ex Assert.Equal(expectedResult, actualResult); } } -} \ No newline at end of file +}