Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Updated SameSite compatibility check #262

Merged
merged 1 commit into from
Jun 30, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 105 additions & 33 deletions src/Microsoft.Identity.Web/CookiePolicyOptionsExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -14,7 +16,7 @@ public static class CookiePolicyOptionsExtensions
{
/// <summary>
/// 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/.
/// </summary>
/// <param name="options"><see cref="CookiePolicyOptions"/>to update.</param>
/// <returns><see cref="CookiePolicyOptions"/> to chain.</returns>
Expand All @@ -25,10 +27,10 @@ public static CookiePolicyOptions HandleSameSiteCookieCompatibility(this CookieP

/// <summary>
/// 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/.
/// </summary>
/// <param name="options"><see cref="CookiePolicyOptions"/>to update.</param>
/// <param name="disallowsSameSiteNone">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/. </param>
/// <param name="disallowsSameSiteNone">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/. </param>
/// <returns><see cref="CookiePolicyOptions"/> to chain.</returns>
public static CookiePolicyOptions HandleSameSiteCookieCompatibility(this CookiePolicyOptions options, Func<string, bool> disallowsSameSiteNone)
{
Expand Down Expand Up @@ -59,50 +61,120 @@ private static void CheckSameSite(HttpContext httpContext, CookieOptions options
}

/// <summary>
/// Checks if the specified user agent supports SameSite None cookies.
/// Checks if the specified user agent supports "SameSite=None" cookies.
/// </summary>
/// <param name="userAgent">Browser user agent.</param>
/// <remarks>Method taken from https://devblogs.microsoft.com/aspnet/upcoming-samesite-cookie-changes-in-asp-net-and-asp-net-core/.</remarks>
/// <returns>True, if the user agent does not allow SameSite None cookie; otherwise, false.</returns>
/// <remarks>
/// Incompatible user agents include:
/// <list type="bullet">
/// <item>Versions of Chrome from Chrome 51 to Chrome 66 (inclusive on both ends).</item>
/// <item>Versions of UC Browser on Android prior to version 12.13.2.</item>
/// <item>Versions of Safari and embedded browsers on MacOS 10.14 and all browsers on iOS 12.</item>
/// </list>
/// Reference: https://www.chromium.org/updates/same-site/incompatible-clients.
/// </remarks>
/// <returns>True, if the user agent does not allow "SameSite=None" cookie; otherwise, false.</returns>
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;
}
}
}
}
20 changes: 14 additions & 6 deletions src/Microsoft.Identity.Web/Microsoft.Identity.Web.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand All @@ -105,11 +108,15 @@ 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);

Assert.Equal(expectedResult, actualResult);
}
}
}
}