io.undertow.util.SameSiteNoneIncompatibleClientChecker Maven / Gradle / Ivy
/*
* JBoss, Home of Professional Open Source.
* Copyright 2020 Red Hat, Inc., and individual contributors
* as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.undertow.util;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* A utility class that can check known user agents which are known to be incompatible with SameSite=None attribute.
*
*
* - Versions of Chrome from Chrome 51 to Chrome 66 (inclusive on both ends).
* These Chrome versions will reject a cookie with `SameSite=None`. This also
* affects older versions of Chromium-derived browsers, as well as Android WebView.
* This behavior was correct according to the version of the cookie specification
* at that time, but with the addition of the new "None" value to the specification,
* this behavior has been updated in Chrome 67 and newer. (Prior to Chrome 51,
* the SameSite attribute was ignored entirely and all cookies were treated as if
* they were `SameSite=None`.)
* - Versions of UC Browser on Android prior to version 12.13.2. Older versions
* will reject a cookie with `SameSite=None`. This behavior was correct according
* to the version of the cookie specification at that time, but with the addition of
* the new "None" value to the specification, this behavior has been updated in newer
* versions of UC Browser.
*
- Versions of Safari and embedded browsers on MacOS 10.14 and all browsers on iOS 12.
* These versions will erroneously treat cookies marked with `SameSite=None` as if they
* were marked `SameSite=Strict`. This bug has been fixed on newer versions of iOS and MacOS.
*
*
* @see SameSite=None: Known Incompatible Clients.
*/
public final class SameSiteNoneIncompatibleClientChecker {
/**
* User Agents Regex Patterns
*/
private static final Pattern IOS_PATTERN = Pattern.compile("\\(iP.+; CPU .*OS (\\d+)[_\\d]*.*\\) AppleWebKit\\/");
private static final Pattern MACOSX_PATTERN = Pattern.compile("\\(Macintosh;.*Mac OS X (\\d+)_(\\d+)[_\\d]*.*\\) AppleWebKit\\/");
private static final Pattern SAFARI_PATTERN = Pattern.compile("Version\\/.* Safari\\/");
private static final Pattern MAC_EMBEDDED_BROWSER_PATTERN = Pattern.compile("^Mozilla\\/[\\.\\d]+ \\(Macintosh;.*Mac OS X [_\\d]+\\) AppleWebKit\\/[\\.\\d]+ \\(KHTML, like Gecko\\)$");
private static final Pattern CHROMIUM_PATTERN = Pattern.compile("Chrom(e|ium)");
private static final Pattern CHROMIUM_VERSION_PATTERN = Pattern.compile("Chrom[^ \\/]+\\/(\\d+)[\\.\\d]* ");
// private static final Pattern UC_BROWSER_PATTERN = Pattern.compile("UCBrowser\\/");
private static final Pattern UC_BROWSER_VERSION_PATTERN = Pattern.compile("UCBrowser\\/(\\d+)\\.(\\d+)\\.(\\d+)[\\.\\d]* ");
public static boolean shouldSendSameSiteNone(String useragent) {
return !isSameSiteNoneIncompatible(useragent);
}
// browsers known to be incompatible.
public static boolean isSameSiteNoneIncompatible(String useragent) {
if (useragent == null || useragent.isEmpty()) {
return false;
}
return hasWebKitSameSiteBug(useragent) ||
dropsUnrecognizedSameSiteCookies(useragent);
}
private static boolean hasWebKitSameSiteBug(String useragent) {
return isIosVersion(12, useragent) ||
(isMacosxVersion(10, 14, useragent) &&
(isSafari(useragent) || isMacEmbeddedBrowser(useragent)));
}
private static boolean dropsUnrecognizedSameSiteCookies(String useragent) {
if (isUcBrowser(useragent)) {
return !isUcBrowserVersionAtLeast(12, 13, 2, useragent);
}
return isChromiumBased(useragent) &&
isChromiumVersionAtLeast(51, useragent) &&
!isChromiumVersionAtLeast(67, useragent);
}
// Regex parsing of User-Agent String. (See note above!)
private static boolean isIosVersion(int major, String useragent) {
Matcher m = IOS_PATTERN.matcher(useragent);
if (m.find()) {
// Extract digits from first capturing group.
return String.valueOf(major).equals(m.group(1));
}
return false;
}
private static boolean isMacosxVersion(int major, int minor, String useragent) {
Matcher m = MACOSX_PATTERN.matcher(useragent);
if (m.find()) {
// Extract digits from first and second capturing groups.
return String.valueOf(major).equals(m.group(1)) &&
String.valueOf(minor).equals(m.group(2));
}
return false;
}
private static boolean isSafari(String useragent) {
return SAFARI_PATTERN.matcher(useragent).find() &&
!isChromiumBased(useragent);
}
private static boolean isMacEmbeddedBrowser(String useragent) {
return MAC_EMBEDDED_BROWSER_PATTERN.matcher(useragent).find();
}
private static boolean isChromiumBased(String useragent) {
return CHROMIUM_PATTERN.matcher(useragent).find();
}
private static boolean isChromiumVersionAtLeast(int major, String useragent) {
Matcher m = CHROMIUM_VERSION_PATTERN.matcher(useragent);
if (m.find()) {
// Extract digits from first capturing group.
int version = Integer.parseInt(m.group(1));
return version >= major;
}
return false;
}
static boolean isUcBrowser(String useragent) {
return useragent.contains("UCBrowser/");
}
private static boolean isUcBrowserVersionAtLeast(int major, int minor, int build, String useragent) {
Matcher m = UC_BROWSER_VERSION_PATTERN.matcher(useragent);
if (m.find()) {
// Extract digits from three capturing groups.
int major_version = Integer.parseInt(m.group(1));
int minor_version = Integer.parseInt(m.group(2));
int build_version = Integer.parseInt(m.group(3));
if (major_version != major) {
return major_version > major;
}
if (minor_version != minor) {
return minor_version > minor;
}
return build_version >= build;
}
return false;
}
}