diff --git a/typo3/sysext/core/Classes/Authentication/AbstractUserAuthentication.php b/typo3/sysext/core/Classes/Authentication/AbstractUserAuthentication.php index ecc4e515f2a91b6e7aa54e2bef11d7b22746c9a4..f6ec44d29b5c90ae33b8e4150136cd8095d737b6 100644 --- a/typo3/sysext/core/Classes/Authentication/AbstractUserAuthentication.php +++ b/typo3/sysext/core/Classes/Authentication/AbstractUserAuthentication.php @@ -29,6 +29,7 @@ use TYPO3\CMS\Core\Database\Query\Restriction\QueryRestrictionContainerInterface use TYPO3\CMS\Core\Database\Query\Restriction\RootLevelRestriction; use TYPO3\CMS\Core\Database\Query\Restriction\StartTimeRestriction; use TYPO3\CMS\Core\Exception; +use TYPO3\CMS\Core\Http\CookieHeaderTrait; use TYPO3\CMS\Core\Session\Backend\Exception\SessionNotFoundException; use TYPO3\CMS\Core\Session\Backend\SessionBackendInterface; use TYPO3\CMS\Core\Session\SessionManager; @@ -49,6 +50,7 @@ use TYPO3\CMS\Core\Utility\GeneralUtility; abstract class AbstractUserAuthentication implements LoggerAwareInterface { use LoggerAwareTrait; + use CookieHeaderTrait; /** * Session/Cookie name @@ -452,8 +454,11 @@ abstract class AbstractUserAuthentication implements LoggerAwareInterface $cookieExpire = $isRefreshTimeBasedCookie ? $GLOBALS['EXEC_TIME'] + $this->lifetime : 0; // Use the secure option when the current request is served by a secure connection: $cookieSecure = (bool)$settings['cookieSecure'] && GeneralUtility::getIndpEnv('TYPO3_SSL'); - $cookieSameSite = $this->getCookieSameSite(); - // None needs the secure option (only allowed on HTTPS) + // Valid options are "strict", "lax" or "none", whereas "none" only works in HTTPS requests (default & fallback is "strict") + $cookieSameSite = $this->sanitizeSameSiteCookieValue( + strtolower($GLOBALS['TYPO3_CONF_VARS'][$this->loginType]['cookieSameSite'] ?? Cookie::SAMESITE_STRICT) + ); + // SameSite "none" needs the secure option (only allowed on HTTPS) if ($cookieSameSite === Cookie::SAMESITE_NONE) { $cookieSecure = true; } @@ -482,24 +487,6 @@ abstract class AbstractUserAuthentication implements LoggerAwareInterface } } - /** - * Fetches the cookie information from the current LocalConfiguration option, based on the $loginType - * which is either "BE" or "FE". - * Valid options are "strict", "lax" or "none", whereas "none" only works in HTTPS requests. - * - * If nothing is defined, or a wrong value is defined, a fallback to "strict" is put in place. - * - * @return string - */ - protected function getCookieSameSite(): string - { - $cookieSameSite = strtolower($GLOBALS['TYPO3_CONF_VARS'][$this->loginType]['cookieSameSite'] ?? Cookie::SAMESITE_STRICT); - if (!in_array($cookieSameSite, [Cookie::SAMESITE_STRICT, Cookie::SAMESITE_LAX, Cookie::SAMESITE_NONE], true)) { - $cookieSameSite = Cookie::SAMESITE_STRICT; - } - return $cookieSameSite; - } - /** * Gets the domain to be used on setting cookies. * The information is taken from the value in $GLOBALS['TYPO3_CONF_VARS']['SYS']['cookieDomain']. diff --git a/typo3/sysext/core/Classes/Http/CookieHeaderTrait.php b/typo3/sysext/core/Classes/Http/CookieHeaderTrait.php new file mode 100644 index 0000000000000000000000000000000000000000..5432a4578d167901eb4f44b7186ec412c88d5ebb --- /dev/null +++ b/typo3/sysext/core/Classes/Http/CookieHeaderTrait.php @@ -0,0 +1,74 @@ +<?php +declare(strict_types = 1); +namespace TYPO3\CMS\Core\Http; + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +use Symfony\Component\HttpFoundation\Cookie; + +trait CookieHeaderTrait +{ + private function hasSameSiteCookieSupport(): bool + { + return version_compare(PHP_VERSION, '7.3.0', '>='); + } + + /** + * Since PHP < 7.3 is not capable of sending the same-site cookie information, session_start() effectively + * sends the Set-Cookie header. This method fetches the set-cookie headers, parses it via Symfony's Cookie + * object, and resends the header. + * + * @param string[] $cookieNames + */ + private function resendCookieHeader(array $cookieNames = []): void + { + $cookies = array_filter(headers_list(), function (string $header) { + return stripos($header, 'Set-Cookie:') === 0; + }); + $cookies = array_map(function (string $cookieHeader) use ($cookieNames) { + $payload = ltrim(substr($cookieHeader, 11)); + $cookie = Cookie::fromString($payload); + $sameSite = $cookie->getSameSite(); + // adjust SameSite flag only for given cookie names (applied to all if not declared) + if (empty($cookieNames) || in_array($cookie->getName(), $cookieNames, true)) { + $sameSite = $sameSite ?? Cookie::SAMESITE_STRICT; + } + return (string)Cookie::create( + $cookie->getName(), + $cookie->getValue(), + $cookie->getExpiresTime(), + $cookie->getPath(), + $cookie->getDomain(), + $cookie->isSecure(), + $cookie->isHttpOnly(), + $cookie->isRaw(), + $sameSite + ); + }, $cookies); + if (!empty($cookies)) { + header_remove('Set-Cookie'); + foreach ($cookies as $cookie) { + header('Set-Cookie: ' . $cookie, false); + } + } + } + + private function sanitizeSameSiteCookieValue(string $cookieSameSite): string + { + if (!in_array($cookieSameSite, [Cookie::SAMESITE_STRICT, Cookie::SAMESITE_LAX, Cookie::SAMESITE_NONE], true)) { + $cookieSameSite = Cookie::SAMESITE_STRICT; + } + return $cookieSameSite; + } +} diff --git a/typo3/sysext/install/Classes/Service/SessionService.php b/typo3/sysext/install/Classes/Service/SessionService.php index 92593cc85eae9e8390ade46f9ea8fc262cf787f0..86069a1e8f3772a3c4d9e3894ca1066b79a1bbc2 100644 --- a/typo3/sysext/install/Classes/Service/SessionService.php +++ b/typo3/sysext/install/Classes/Service/SessionService.php @@ -16,6 +16,7 @@ namespace TYPO3\CMS\Install\Service; use Symfony\Component\HttpFoundation\Cookie; use TYPO3\CMS\Core\Core\Environment; +use TYPO3\CMS\Core\Http\CookieHeaderTrait; use TYPO3\CMS\Core\Messaging\FlashMessage; use TYPO3\CMS\Core\SingletonInterface; use TYPO3\CMS\Core\Utility\GeneralUtility; @@ -26,6 +27,8 @@ use TYPO3\CMS\Core\Utility\GeneralUtility; */ class SessionService implements SingletonInterface { + use CookieHeaderTrait; + /** * The path to our var/ folder (where we can write our sessions). Set in the * constructor. @@ -78,7 +81,7 @@ class SessionService implements SingletonInterface session_save_path($sessionSavePath); session_name($this->cookieName); ini_set('session.cookie_httponly', true); - if (PHP_VERSION_ID >= 70300) { + if ($this->hasSameSiteCookieSupport()) { ini_set('session.cookie_samesite', Cookie::SAMESITE_STRICT); } ini_set('session.cookie_path', (string)GeneralUtility::getIndpEnv('TYPO3_SITE_PATH')); @@ -98,44 +101,11 @@ class SessionService implements SingletonInterface throw new \TYPO3\CMS\Install\Exception($sessionCreationError, 1294587486); } session_start(); - if (PHP_VERSION_ID < 70300) { + if (!$this->hasSameSiteCookieSupport()) { $this->resendCookieHeader(); } } - /** - * Since PHP < 7.3 is not capable of sending the same-site cookie information, session_start() effectively - * sends the Set-Cookie header. This method fetches the set-cookie headers, parses it via Symfony's Cookie - * object, and resends the header. - */ - private function resendCookieHeader() - { - $cookies = array_filter(headers_list(), function (string $header) { - return stripos($header, 'Set-Cookie:') === 0; - }); - $cookies = array_map(function (string $cookieHeader) { - $payload = ltrim(substr($cookieHeader, 11)); - $cookie = Cookie::fromString($payload); - return (string)Cookie::create( - $cookie->getName(), - $cookie->getValue(), - $cookie->getExpiresTime(), - $cookie->getPath(), - $cookie->getDomain(), - $cookie->isSecure(), - $cookie->isHttpOnly(), - $cookie->isRaw(), - $cookie->getSameSite() ?? Cookie::SAMESITE_STRICT - ); - }, $cookies); - if (!empty($cookies)) { - header_remove('Set-Cookie'); - foreach ($cookies as $cookie) { - header('Set-Cookie: ' . $cookie, false); - } - } - } - /** * Returns the path where to store our session files * @@ -234,6 +204,9 @@ class SessionService implements SingletonInterface private function renewSession() { session_regenerate_id(); + if (!$this->hasSameSiteCookieSupport()) { + $this->resendCookieHeader([$this->cookieName]); + } return session_id(); } diff --git a/typo3/sysext/workspaces/Classes/Middleware/WorkspacePreview.php b/typo3/sysext/workspaces/Classes/Middleware/WorkspacePreview.php index 3d450b119c06031eb67aa16481389fa455674a5b..84c308af5f25c5925aca81bfb60c2d1cc3b7b93a 100644 --- a/typo3/sysext/workspaces/Classes/Middleware/WorkspacePreview.php +++ b/typo3/sysext/workspaces/Classes/Middleware/WorkspacePreview.php @@ -19,11 +19,13 @@ use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; +use Symfony\Component\HttpFoundation\Cookie; use TYPO3\CMS\Core\Authentication\BackendUserAuthentication; use TYPO3\CMS\Core\Context\Context; use TYPO3\CMS\Core\Context\UserAspect; use TYPO3\CMS\Core\Context\WorkspaceAspect; use TYPO3\CMS\Core\Database\ConnectionPool; +use TYPO3\CMS\Core\Http\CookieHeaderTrait; use TYPO3\CMS\Core\Http\HtmlResponse; use TYPO3\CMS\Core\Http\NormalizedParams; use TYPO3\CMS\Core\Http\Stream; @@ -43,6 +45,8 @@ use TYPO3\CMS\Workspaces\Authentication\PreviewUserAuthentication; */ class WorkspacePreview implements MiddlewareInterface { + use CookieHeaderTrait; + /** * The GET parameter to be used (also the cookie name) * @@ -221,7 +225,24 @@ class WorkspacePreview implements MiddlewareInterface */ protected function setCookie(string $inputCode, NormalizedParams $normalizedParams) { - setcookie($this->previewKey, $inputCode, 0, $normalizedParams->getSitePath(), '', true, true); + $cookieSameSite = $this->sanitizeSameSiteCookieValue( + strtolower($GLOBALS['TYPO3_CONF_VARS']['BE']['cookieSameSite'] ?? Cookie::SAMESITE_STRICT) + ); + // None needs the secure option (only allowed on HTTPS) + $cookieSecure = $cookieSameSite === Cookie::SAMESITE_NONE || $normalizedParams->isHttps(); + + $cookie = new Cookie( + $this->previewKey, + $inputCode, + 0, + $normalizedParams->getSitePath(), + null, + $cookieSecure, + true, + false, + $cookieSameSite + ); + header('Set-Cookie: ' . $cookie->__toString(), false); } /**