From 0e49d2809be8db99be72082fca0446c4b028d3f1 Mon Sep 17 00:00:00 2001 From: ignetic Date: Tue, 18 Aug 2020 10:22:10 +0100 Subject: [PATCH] Cater for SameSite=None incompatible browsers Not all browsers are compatible with SameSite=None. This will handle those that are not compatible and leave the parameter blank. --- README.md | 3 + samesite_cookies/README.md | 3 + samesite_cookies/addon.setup.php | 2 +- samesite_cookies/ext.samesite_cookies.php | 58 +++++--- .../uvii/samesitenone/src/SameSiteNone.php | 131 ++++++++++++++++++ 5 files changed, 175 insertions(+), 22 deletions(-) create mode 100644 samesite_cookies/vendor/uvii/samesitenone/src/SameSiteNone.php diff --git a/README.md b/README.md index ca898bd..3f38d9c 100644 --- a/README.md +++ b/README.md @@ -40,3 +40,6 @@ With recent changes to Google Chrome, cookies are now defaulted to SameSite=Lax. This addon can resolve issues where offsite cookies are required, such as offsite payment gateways and with 3D Secure. +Note that not all browsers are compatible with SameSite=None. This addon will handle those that are not compatible and leave the parameter blank. +https://www.chromium.org/updates/same-site/incompatible-clients + diff --git a/samesite_cookies/README.md b/samesite_cookies/README.md index ca898bd..3f38d9c 100644 --- a/samesite_cookies/README.md +++ b/samesite_cookies/README.md @@ -40,3 +40,6 @@ With recent changes to Google Chrome, cookies are now defaulted to SameSite=Lax. This addon can resolve issues where offsite cookies are required, such as offsite payment gateways and with 3D Secure. +Note that not all browsers are compatible with SameSite=None. This addon will handle those that are not compatible and leave the parameter blank. +https://www.chromium.org/updates/same-site/incompatible-clients + diff --git a/samesite_cookies/addon.setup.php b/samesite_cookies/addon.setup.php index d8e8621..0515e54 100644 --- a/samesite_cookies/addon.setup.php +++ b/samesite_cookies/addon.setup.php @@ -5,7 +5,7 @@ 'author_url' => 'https://github.com/ignetic', 'name' => 'SameSite Cookies', 'description' => 'Add SameSite attribute to ExpressionEngine cookies', - 'version' => '1.1', + 'version' => '1.2', 'namespace' => '\\', 'settings_exist' => TRUE, ); diff --git a/samesite_cookies/ext.samesite_cookies.php b/samesite_cookies/ext.samesite_cookies.php index 1b90aa3..1e35f83 100644 --- a/samesite_cookies/ext.samesite_cookies.php +++ b/samesite_cookies/ext.samesite_cookies.php @@ -1,11 +1,15 @@ session->userdata('user_agent'); + $SameSiteNoneSafe = SameSiteNone::isSafe($userAgent); + if (isset($this->settings['cookies']) && ! empty($this->settings['cookies'])) { $cookies = explode("\n", str_replace(",", "\n", trim($this->settings['cookies']))); $cookies = array_map('trim', $cookies); - - $cookieName = $data['prefix'].$data['name']; - $data['samesite'] = (isset($this->settings['samesite']) ? $this->settings['samesite'] : ''); - - if (isset($this->settings['secure_cookies']) && $this->settings['secure_cookies'] === 'yes') - { - $data['secure_cookie'] = 1; - } - - if (isset($this->settings['all_cookies']) && $this->settings['all_cookies'] === 'apply_all') - { - $return = $this->set_samesite_cookie($data); - ee()->extensions->end_script = TRUE; - } - else if (in_array($cookieName, $cookies)) - { - $return = $this->set_samesite_cookie($data); - ee()->extensions->end_script = TRUE; - } } + + $data['samesite'] = (isset($this->settings['samesite']) ? $this->settings['samesite'] : ''); + + if ( ! $SameSiteNoneSafe && $data['samesite'] == 'None') + { + $data['samesite'] = ''; + } + + if (isset($this->settings['secure_cookies']) && $this->settings['secure_cookies'] === 'yes') + { + $data['secure_cookie'] = 1; + } + + if (isset($this->settings['all_cookies']) && $this->settings['all_cookies'] === 'apply_all') + { + $return = $this->set_samesite_cookie($data); + ee()->extensions->end_script = TRUE; + } + else if (in_array($data['prefix'].$data['name'], $cookies)) + { + $return = $this->set_samesite_cookie($data); + ee()->extensions->end_script = TRUE; + } + return $return; } @@ -110,7 +126,7 @@ private function set_samesite_cookie($data) // thus the SameSite setting must be hacked in with the path option. return setcookie($data['prefix'].$data['name'], $data['value'], $data['expire'], - $data['path'] . '; SameSite=' . $data['samesite'], + $data['path'] . ( ! empty($data['samesite']) ? '; SameSite=' . $data['samesite'] : ''), $data['domain'], $data['secure_cookie'], $data['httponly'] diff --git a/samesite_cookies/vendor/uvii/samesitenone/src/SameSiteNone.php b/samesite_cookies/vendor/uvii/samesitenone/src/SameSiteNone.php new file mode 100644 index 0000000..9f105bb --- /dev/null +++ b/samesite_cookies/vendor/uvii/samesitenone/src/SameSiteNone.php @@ -0,0 +1,131 @@ +uaStr = $uaStr; + } + + public static function isSafe(String $useragent): bool + { + return ((new self($useragent))->shouldSendSameSiteNone()); + } + + function shouldSendSameSiteNone(): bool + { + return !$this->isSameSiteNoneIncompatible(); + } + + function isSameSiteNoneIncompatible(): bool + { + return $this->hasWebKitSameSiteBug() || + $this->dropsUnrecognizedSameSiteCookies(); + } + + function hasWebKitSameSiteBug(): bool + { + return $this->isIosVersion(12) || + ($this->isMacosxVersion(10,14) && + ($this->isSafari() || + $this->isMacEmbeddedBrowser() + ) + ); + } + + function dropsUnrecognizedSameSiteCookies(): bool + { + if ($this->isUcBrowser()) { + return !$this->isUcBrowserVersionAtLeast(12,13,2); + } + return $this->isChromiumBased() && + $this->isChromiumVersionAtLeast(51) && + !$this->isChromiumVersionAtLeast(67); + } + + public function isIosVersion(int $major): bool + { + + $regex = "/\(iP.+; CPU .*OS (\d+)[_\d]*.*\) AppleWebKit\//"; + $ver = 0; + if (preg_match($regex, $this->uaStr, $matches)) { + $ver = intval($matches[1]); + } + return $ver === $major; + } + + public function isMacosxVersion(int $major, int $minor): bool + { + + $regex = "/\(Macintosh;.*Mac OS X (\d+)_(\d+)[_\d]*.*\) AppleWebKit\//"; + $major_version = 0; + $minor_version = 0; + if (preg_match($regex, $this->uaStr, $matches)) { + $major_version = intval($matches[1]); + $minor_version = intval($matches[2]); + } + return ($major_version === $major) && + ($minor_version === $minor); + } + + public function isSafari(): bool + { + $regex = "/Version\/.* Safari\//"; + return (1 === preg_match($regex, $this->uaStr)) && + !$this->isChromiumBased(); + } + + public function isMacEmbeddedBrowser(): bool + { + $regex = "/^Mozilla\/[\.\d]+ \(Macintosh;.*Mac OS X [_\d]+\) AppleWebKit\/[\.\d]+ \(KHTML, like Gecko\)$/"; + return (1 === preg_match($regex, $this->uaStr)); + } + + public function isChromiumBased(): bool + { + $regex = "/Chrom(e|ium)/"; + return (1 === preg_match($regex, $this->uaStr)); + } + + public function isChromiumVersionAtLeast(int $major): bool + { + $regex = "/Chrom[^ \/]+\/(\d+)[\.\d]*/"; + $ver = 0; + if (preg_match($regex, $this->uaStr, $matches)) { + $ver = intval($matches[1]); + } + return $ver >= $major; + } + + public function isUcBrowser(): bool + { + $regex = "/UCBrowser/"; + return (1 === preg_match($regex, $this->uaStr)) ? true: false; + } + + public function isUcBrowserVersionAtLeast(int $major, int $minor, int $build): bool + { + $regex = "/UCBrowser\/(\d+)\.(\d+)\.(\d+)[\.\d]* /"; + $major_version = 0; + $minor_version = 0; + $build_version = 0; + if (preg_match($regex, $this->uaStr, $matches)) { + $major_version = intval($matches[1]); + $minor_version = intval($matches[2]); + $build_version = intval($matches[3]); + } + if ($major_version != $major) { + return $major_version > $major; + } + if ($minor_version != $minor) { + return $minor_version > $minor; + } + return $build_version >= $build; + } +} + +