diff --git a/src/Microsoft.Identity.Web/CookiePolicyOptionsExtensions.cs b/src/Microsoft.Identity.Web/CookiePolicyOptionsExtensions.cs index 244841d3d..ca1568e46 100644 --- a/src/Microsoft.Identity.Web/CookiePolicyOptionsExtensions.cs +++ b/src/Microsoft.Identity.Web/CookiePolicyOptionsExtensions.cs @@ -13,7 +13,7 @@ namespace Microsoft.Identity.Web /// /// Extension class containing cookie policies (work around for same site). /// - public static class CookiePolicyOptionsExtensions + public static partial class CookiePolicyOptionsExtensions { private const int Two = 2; private const int SixtySeven = 67; @@ -22,6 +22,12 @@ public static class CookiePolicyOptionsExtensions private const int Twelve = 12; private const int Ten = 10; private const int Fourteen = 14; + private const int MaxUserAgentLength = 512; + +#if !NET7_0_OR_GREATER + private static readonly TimeSpan UserAgentRegexTimeout = TimeSpan.FromMilliseconds(100); + private const RegexOptions UserAgentRegexOptions = RegexOptions.None; +#endif /// /// Handles SameSite cookies according to the ASP.NET Core documentation at https://learn.microsoft.com/aspnet/core/security/samesite. @@ -62,6 +68,12 @@ private static void CheckSameSite(HttpContext httpContext, CookieOptions options if (options.SameSite == SameSiteMode.None) { var userAgent = httpContext.Request.Headers[Constants.UserAgent].ToString(); + + if (userAgent.Length > MaxUserAgentLength) + { + return; + } + if (disallowsSameSiteNone(userAgent)) { options.SameSite = SameSiteMode.Unspecified; @@ -69,6 +81,32 @@ private static void CheckSameSite(HttpContext httpContext, CookieOptions options } } +#if NET7_0_OR_GREATER + [GeneratedRegex(@"\(iP.+; CPU .*OS (\d+)(?:_\d+)*[^)]*\) AppleWebKit\/", RegexOptions.NonBacktracking)] + private static partial Regex IosVersionRegex(); + + [GeneratedRegex(@"\(Macintosh;.*Mac OS X (\d+)_(\d+)(?:_\d+)*[^)]*\) AppleWebKit\/", RegexOptions.NonBacktracking)] + private static partial Regex MacosxVersionRegex(); + + [GeneratedRegex(@"Version\/.* Safari\/", RegexOptions.NonBacktracking)] + private static partial Regex SafariRegex(); + + [GeneratedRegex(@"^Mozilla\/[\.\d]+ \(Macintosh;.*Mac OS X [_\d]+\) AppleWebKit\/[\.\d]+ \(KHTML, like Gecko\)$", RegexOptions.NonBacktracking)] + private static partial Regex MacEmbeddedBrowserRegex(); + + [GeneratedRegex("Chrom(e|ium)", RegexOptions.NonBacktracking)] + private static partial Regex ChromiumBasedRegex(); + + [GeneratedRegex(@"Chrom[^ \/]+\/(\d+)[\.\d]*", RegexOptions.NonBacktracking)] + private static partial Regex ChromiumVersionRegex(); + + [GeneratedRegex(@"UCBrowser\/", RegexOptions.NonBacktracking)] + private static partial Regex UcBrowserRegex(); + + [GeneratedRegex(@"UCBrowser\/(\d+)\.(\d+)\.(\d+)[\.\d]* ", RegexOptions.NonBacktracking)] + private static partial Regex UcBrowserVersionRegex(); +#endif + /// /// Checks if the specified user agent supports "SameSite=None" cookies. /// @@ -85,8 +123,15 @@ private static void CheckSameSite(HttpContext httpContext, CookieOptions options /// True, if the user agent does not allow "SameSite=None" cookie; otherwise, false. public static bool DisallowsSameSiteNone(string userAgent) { - return HasWebKitSameSiteBug() || - DropsUnrecognizedSameSiteCookies(); + try + { + return HasWebKitSameSiteBug() || + DropsUnrecognizedSameSiteCookies(); + } + catch (RegexMatchTimeoutException) + { + return false; + } bool HasWebKitSameSiteBug() => IsIosVersion(Twelve) || @@ -107,51 +152,60 @@ bool DropsUnrecognizedSameSiteCookies() bool IsIosVersion(int major) { - const string regex = @"\(iP.+; CPU .*OS (\d+)[_\d]*.*\) AppleWebKit\/"; - - // Extract digits from first capturing group. - Match match = Regex.Match(userAgent, regex); +#if NET7_0_OR_GREATER + Match match = IosVersionRegex().Match(userAgent); +#else + Match match = Regex.Match(userAgent, @"\(iP.+; CPU .*OS (\d+)(?:_\d+)*[^)]*\) AppleWebKit\/", UserAgentRegexOptions, UserAgentRegexTimeout); +#endif return match.Groups[1].Value == major.ToString(CultureInfo.CurrentCulture); } bool IsMacosxVersion(int major, int minor) { - const string regex = @"\(Macintosh;.*Mac OS X (\d+)_(\d+)[_\d]*.*\) AppleWebKit\/"; - - // Extract digits from first and second capturing groups. - Match match = Regex.Match(userAgent, regex); +#if NET7_0_OR_GREATER + Match match = MacosxVersionRegex().Match(userAgent); +#else + Match match = Regex.Match(userAgent, @"\(Macintosh;.*Mac OS X (\d+)_(\d+)(?:_\d+)*[^)]*\) AppleWebKit\/", UserAgentRegexOptions, UserAgentRegexTimeout); +#endif return match.Groups[1].Value == major.ToString(CultureInfo.CurrentCulture) && match.Groups[Two].Value == minor.ToString(CultureInfo.CurrentCulture); } bool IsSafari() { - const string regex = @"Version\/.* Safari\/"; - - return Regex.IsMatch(userAgent, regex) && +#if NET7_0_OR_GREATER + return SafariRegex().IsMatch(userAgent) && !IsChromiumBased(); +#else + return Regex.IsMatch(userAgent, @"Version\/.* Safari\/", UserAgentRegexOptions, UserAgentRegexTimeout) && !IsChromiumBased(); +#endif } bool IsMacEmbeddedBrowser() { - const string regex = @"^Mozilla\/[\.\d]+ \(Macintosh;.*Mac OS X [_\d]+\) AppleWebKit\/[\.\d]+ \(KHTML, like Gecko\)$"; - - return Regex.IsMatch(userAgent, regex); +#if NET7_0_OR_GREATER + return MacEmbeddedBrowserRegex().IsMatch(userAgent); +#else + return Regex.IsMatch(userAgent, @"^Mozilla\/[\.\d]+ \(Macintosh;.*Mac OS X [_\d]+\) AppleWebKit\/[\.\d]+ \(KHTML, like Gecko\)$", UserAgentRegexOptions, UserAgentRegexTimeout); +#endif } bool IsChromiumBased() { - const string regex = "Chrom(e|ium)"; - - return Regex.IsMatch(userAgent, regex); +#if NET7_0_OR_GREATER + return ChromiumBasedRegex().IsMatch(userAgent); +#else + return Regex.IsMatch(userAgent, "Chrom(e|ium)", UserAgentRegexOptions, UserAgentRegexTimeout); +#endif } bool IsChromiumVersionAtLeast(int major) { - const string regex = @"Chrom[^ \/]+\/(\d+)[\.\d]*"; - - // Extract digits from first capturing group. - Match match = Regex.Match(userAgent, regex); +#if NET7_0_OR_GREATER + Match match = ChromiumVersionRegex().Match(userAgent); +#else + Match match = Regex.Match(userAgent, @"Chrom[^ \/]+\/(\d+)[\.\d]*", UserAgentRegexOptions, UserAgentRegexTimeout); +#endif if (!match.Success) return false; @@ -163,17 +217,20 @@ bool IsChromiumVersionAtLeast(int major) bool IsUcBrowser() { - const string regex = @"UCBrowser\/"; - - return Regex.IsMatch(userAgent, regex); +#if NET7_0_OR_GREATER + return UcBrowserRegex().IsMatch(userAgent); +#else + return Regex.IsMatch(userAgent, @"UCBrowser\/", UserAgentRegexOptions, UserAgentRegexTimeout); +#endif } bool IsUcBrowserVersionAtLeast(int major, int minor, int build) { - const string regex = @"UCBrowser\/(\d+)\.(\d+)\.(\d+)[\.\d]* "; - - // Extract digits from three capturing groups. - Match match = Regex.Match(userAgent, regex); +#if NET7_0_OR_GREATER + Match match = UcBrowserVersionRegex().Match(userAgent); +#else + Match match = Regex.Match(userAgent, @"UCBrowser\/(\d+)\.(\d+)\.(\d+)[\.\d]* ", UserAgentRegexOptions, UserAgentRegexTimeout); +#endif 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); diff --git a/tests/Microsoft.Identity.Web.Test/CookiePolicyOptionsExtensionsTests.cs b/tests/Microsoft.Identity.Web.Test/CookiePolicyOptionsExtensionsTests.cs index 2cc6c2fa1..f68a6c026 100644 --- a/tests/Microsoft.Identity.Web.Test/CookiePolicyOptionsExtensionsTests.cs +++ b/tests/Microsoft.Identity.Web.Test/CookiePolicyOptionsExtensionsTests.cs @@ -119,5 +119,18 @@ public void DisallowsSameSiteNone_VariousUserAgents_ExecutesSuccessfully(bool ex Assert.Equal(expectedResult, actualResult); } + + [Fact] + public void DisallowsSameSiteNone_NotValidUserAgent_ReturnsFailure() + { + var notValidUserAgent = "(iPhone; CPU iPhone OS " + new string('0', 8000); + + var stopwatch = System.Diagnostics.Stopwatch.StartNew(); + var result = CookiePolicyOptionsExtensions.DisallowsSameSiteNone(notValidUserAgent); + stopwatch.Stop(); + + Assert.False(result); + Assert.True(stopwatch.ElapsedMilliseconds < 200); + } } }