diff --git a/composer.json b/composer.json index aabd8e79d576..db95bc5a2048 100644 --- a/composer.json +++ b/composer.json @@ -58,6 +58,7 @@ "symfony/dependency-injection": "^4.4 || ^5.0", "symfony/expression-language": "^4.4 || ^5.0", "symfony/finder": "^4.4 || ^5.0", + "symfony/http-foundation": "^4.4 || ^5.0", "symfony/mailer": "^4.4 || ^5.0", "symfony/mime": "^4.4 || ^5.0", "symfony/polyfill-intl-icu": "^1.6", diff --git a/composer.lock b/composer.lock index 9f7280966066..9cf2eb8d6de4 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "a1bf7ee0d439e8f2928c9abb5ec33f8b", + "content-hash": "d3d4389879ab79c0d16fdf02f5337f2e", "packages": [ { "name": "cogpowered/finediff", @@ -2046,6 +2046,61 @@ "homepage": "https://symfony.com", "time": "2019-11-17T21:56:56+00:00" }, + { + "name": "symfony/http-foundation", + "version": "v5.0.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-foundation.git", + "reference": "83eb54b75f5365722d4ccdb6559fb099e799202e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/83eb54b75f5365722d4ccdb6559fb099e799202e", + "reference": "83eb54b75f5365722d4ccdb6559fb099e799202e", + "shasum": "" + }, + "require": { + "php": "^7.2.5", + "symfony/mime": "^4.4|^5.0", + "symfony/polyfill-mbstring": "~1.1" + }, + "require-dev": { + "predis/predis": "~1.0", + "symfony/expression-language": "^4.4|^5.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpFoundation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony HttpFoundation Component", + "homepage": "https://symfony.com", + "time": "2019-11-28T14:20:16+00:00" + }, { "name": "symfony/inflector", "version": "v4.4.0", @@ -6990,61 +7045,6 @@ "homepage": "https://symfony.com", "time": "2019-12-01T08:46:01+00:00" }, - { - "name": "symfony/http-foundation", - "version": "v5.0.1", - "source": { - "type": "git", - "url": "https://github.com/symfony/http-foundation.git", - "reference": "83eb54b75f5365722d4ccdb6559fb099e799202e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/83eb54b75f5365722d4ccdb6559fb099e799202e", - "reference": "83eb54b75f5365722d4ccdb6559fb099e799202e", - "shasum": "" - }, - "require": { - "php": "^7.2.5", - "symfony/mime": "^4.4|^5.0", - "symfony/polyfill-mbstring": "~1.1" - }, - "require-dev": { - "predis/predis": "~1.0", - "symfony/expression-language": "^4.4|^5.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.0-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\HttpFoundation\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony HttpFoundation Component", - "homepage": "https://symfony.com", - "time": "2019-11-28T14:20:16+00:00" - }, { "name": "symfony/http-kernel", "version": "v4.4.1", diff --git a/typo3/sysext/backend/Classes/Controller/LoginController.php b/typo3/sysext/backend/Classes/Controller/LoginController.php index 440c20eb6d54..ccb52ad46579 100644 --- a/typo3/sysext/backend/Classes/Controller/LoginController.php +++ b/typo3/sysext/backend/Classes/Controller/LoginController.php @@ -20,6 +20,7 @@ use Psr\Http\Message\ServerRequestInterface; use Psr\Log\LoggerAwareInterface; use Psr\Log\LoggerAwareTrait; +use Symfony\Component\HttpFoundation\Cookie; use TYPO3\CMS\Backend\LoginProvider\Event\ModifyPageLayoutOnLoginProviderSelectionEvent; use TYPO3\CMS\Backend\LoginProvider\LoginProviderInterface; use TYPO3\CMS\Backend\Routing\UriBuilder; @@ -30,6 +31,7 @@ use TYPO3\CMS\Core\FormProtection\BackendFormProtection; use TYPO3\CMS\Core\FormProtection\FormProtectionFactory; use TYPO3\CMS\Core\Http\HtmlResponse; +use TYPO3\CMS\Core\Http\NormalizedParams; use TYPO3\CMS\Core\Information\Typo3Information; use TYPO3\CMS\Core\Localization\LanguageService; use TYPO3\CMS\Core\Localization\Locales; @@ -565,11 +567,24 @@ protected function detectLoginProvider(ServerRequestInterface $request): string reset($this->loginProviders); $loginProvider = key($this->loginProviders); } - // Use the secure option when the current request is served by a secure connection: + // Use the secure option when the current request is served by a secure connection + /** @var NormalizedParams $normalizedParams */ $normalizedParams = $request->getAttribute('normalizedParams'); $isHttps = $normalizedParams->isHttps(); $cookieSecure = (bool)$GLOBALS['TYPO3_CONF_VARS']['SYS']['cookieSecure'] && $isHttps; - setcookie('be_lastLoginProvider', (string)$loginProvider, $GLOBALS['EXEC_TIME'] + 7776000, '', '', $cookieSecure, true); // 90 days + $cookie = new Cookie( + 'be_lastLoginProvider', + (string)$loginProvider, + $GLOBALS['EXEC_TIME'] + 7776000, // 90 days + $normalizedParams->getSitePath() . TYPO3_mainDir, + '', + $cookieSecure, + true, + false, + Cookie::SAMESITE_STRICT + ); + header('Set-Cookie: ' . $cookie->__toString(), false); + return (string)$loginProvider; } diff --git a/typo3/sysext/core/Classes/Authentication/AbstractUserAuthentication.php b/typo3/sysext/core/Classes/Authentication/AbstractUserAuthentication.php index b75abf196e93..ecc4e515f2a9 100644 --- a/typo3/sysext/core/Classes/Authentication/AbstractUserAuthentication.php +++ b/typo3/sysext/core/Classes/Authentication/AbstractUserAuthentication.php @@ -16,6 +16,7 @@ use Psr\Log\LoggerAwareInterface; use Psr\Log\LoggerAwareTrait; +use Symfony\Component\HttpFoundation\Cookie; use TYPO3\CMS\Core\Core\Environment; use TYPO3\CMS\Core\Crypto\Random; use TYPO3\CMS\Core\Database\Connection; @@ -451,9 +452,25 @@ protected function setSessionCookie() $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) + if ($cookieSameSite === Cookie::SAMESITE_NONE) { + $cookieSecure = true; + } // Do not set cookie if cookieSecure is set to "1" (force HTTPS) and no secure channel is used: if ((int)$settings['cookieSecure'] !== 1 || GeneralUtility::getIndpEnv('TYPO3_SSL')) { - setcookie($this->name, $this->id, $cookieExpire, $cookiePath, $cookieDomain, $cookieSecure, true); + $cookie = new Cookie( + $this->name, + $this->id, + $cookieExpire, + $cookiePath, + $cookieDomain, + $cookieSecure, + true, + false, + $cookieSameSite + ); + header('Set-Cookie: ' . $cookie->__toString(), false); $this->cookieWasSetOnCurrentRequest = true; } else { throw new Exception('Cookie was not set since HTTPS was forced in $TYPO3_CONF_VARS[SYS][cookieSecure].', 1254325546); @@ -465,6 +482,24 @@ protected function setSessionCookie() } } + /** + * 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/Configuration/DefaultConfiguration.php b/typo3/sysext/core/Configuration/DefaultConfiguration.php index 891b0f9cab91..5366f2bcea0a 100644 --- a/typo3/sysext/core/Configuration/DefaultConfiguration.php +++ b/typo3/sysext/core/Configuration/DefaultConfiguration.php @@ -1082,6 +1082,7 @@ 'enabledBeUserIPLock' => true, 'cookieDomain' => '', 'cookieName' => 'be_typo_user', + 'cookieSameSite' => 'strict', 'loginSecurityLevel' => 'normal', 'showRefreshLoginPopup' => false, 'adminOnly' => 0, @@ -1262,6 +1263,7 @@ 'permalogin' => 0, 'cookieDomain' => '', 'cookieName' => 'fe_typo_user', + 'cookieSameSite' => 'lax', 'defaultUserTSconfig' => '', 'defaultTypoScript_constants' => '', 'defaultTypoScript_constants.' => [], // Lines of TS to include after a static template with the uid = the index in the array (Constants) diff --git a/typo3/sysext/core/Configuration/DefaultConfigurationDescription.yaml b/typo3/sysext/core/Configuration/DefaultConfigurationDescription.yaml index 7ca838dd83be..8b07f1efcad2 100644 --- a/typo3/sysext/core/Configuration/DefaultConfigurationDescription.yaml +++ b/typo3/sysext/core/Configuration/DefaultConfigurationDescription.yaml @@ -314,6 +314,13 @@ BE: cookieName: type: text description: 'Set the name for the cookie used for the back-end user session' + cookieSameSite: + type: text + allowedValues: + 'lax': 'Cookies set by TYPO3 are only available for the current site, third-party integrations are not allowed to read cookies, except for links and simple HTML forms' + 'strict': 'Cookies sent by TYPO3 are only available for the current site, never shared to other third-party packages' + 'none': 'Allow cookies set by TYPO3 to be sent to other sites as well, please note - this only works with HTTPS connections' + description: 'Indicates that the cookie should send proper information where the cookie can be shared (first-party cookies vs. third-party cookies) in TYPO3 Backend.' loginSecurityLevel: type: text description: 'Keywords that determines the security level of login to the backend. "normal" means the password from the login form is sent in clear-text. The client/server communication should be secured with HTTPS.' @@ -447,6 +454,13 @@ FE: cookieName: type: text description: 'Set the name for the cookie used for the front-end user session' + cookieSameSite: + type: text + allowedValues: + 'lax': 'Cookies set by TYPO3 are only available for the current site, third-party integrations are not allowed to read cookies, except for links and simple HTML forms' + 'strict': 'Cookies sent by TYPO3 are only available for the current site, never shared to other third-party packages' + 'none': 'Allow cookies set by TYPO3 to be sent to other sites as well, please note - this only works with HTTPS connections' + description: 'Indicates that the cookie should send proper information where the cookie can be shared (first-party cookies vs. third-party cookies) in TYPO3 Frontend.' defaultUserTSconfig: type: multiline description: 'Enter lines of default frontend user/group TSconfig.' diff --git a/typo3/sysext/core/Documentation/Changelog/8.7.x/Feature-90351-ConfigureTYPO3-shippedCookiesWithSameSiteFlag.rst b/typo3/sysext/core/Documentation/Changelog/8.7.x/Feature-90351-ConfigureTYPO3-shippedCookiesWithSameSiteFlag.rst new file mode 100644 index 000000000000..0608816662e6 --- /dev/null +++ b/typo3/sysext/core/Documentation/Changelog/8.7.x/Feature-90351-ConfigureTYPO3-shippedCookiesWithSameSiteFlag.rst @@ -0,0 +1,60 @@ +.. include:: ../../Includes.txt + +==================================================================== +Feature: #90351 - Configure TYPO3-shipped cookies with SameSite flag +==================================================================== + +See :issue:`90351` + +Description +=========== + +TYPO3 Core sends four cookies set by PHP to the browser when a session is requested: + +- fe_typo_user - used to identify a session ID when logged-in to the TYPO3 Frontend +- be_typo_user - used to identify a backend session when a Backend User logged in to TYPO3 Backend or Frontend +- Typo3InstallTool - used to validate a session for the System Maintenance Area / "Install Tool" +- be_lastLoginProvider - stores information about the last login provider when logging into TYPO3 Backend + +All modern wide-spread browsers (Mozilla Firefox, Chromium-based Browsers such as Google Chrome, Safari, Microsoft Edge) support sending cookies with an additional flag called "SameSite" which +defines the visibility of a cookie when used in other scripts or +iframes such as a YouTube video embedded into a site. The same site +flag defines whether to send such information to these "third-party +sites". + +Starting with Google Chrome 80 (expected in February 2020), the browser treats any cookie without having the SameSite flag sent to +be the same as "lax". + +TYPO3 now supports the configuration of this cookie for Frontend- +and Backend users. For the install Tool and lastLoginProvider +the cookies are now always sent with the "strict" flag set. + +SameSite enhances privacy for every visitor or editor of your +TYPO3 installation. + +Read more about SameSite cookies on: https://web.dev/samesite-cookies-explained/ + + +Impact +====== + +All cookies sent by TYPO3 Core now send the SameSite flag by default, whereas TYPO3 Frontend sends the SameSite flag "lax", +and all other cookies are sent via "strict". + +The cookies for Frontend User Sessions can be configured via +`$GLOBALS[TYPO3_CONF_VARS][FE][cookieSameSite]` to be either +"strict", "lax" or "none". + +The cookies for Backend User Sessions can be configured via +`$GLOBALS[TYPO3_CONF_VARS][BE][cookieSameSite]` to be either +"strict", "lax" or "none". + +Please note that "none" only works when running the site via HTTPS. + +Older browsers without SameSite support do not consider evaluating +the SameSite flag will behave as before. + +Both settings can be configured in the Install Tool / Maintenance +Area Settings module. + +.. index:: LocalConfiguration, ext:core \ No newline at end of file diff --git a/typo3/sysext/core/composer.json b/typo3/sysext/core/composer.json index c9eaddd3161e..214ae4292904 100644 --- a/typo3/sysext/core/composer.json +++ b/typo3/sysext/core/composer.json @@ -39,6 +39,7 @@ "symfony/dependency-injection": "^4.4 || ^5.0", "symfony/expression-language": "^4.4 || ^5.0", "symfony/finder": "^4.4 || ^5.0", + "symfony/http-foundation": "^4.4 || ^5.0", "symfony/mailer": "^4.4 || ^5.0", "symfony/mime": "^4.4 || ^5.0", "symfony/polyfill-intl-icu": "^1.6", diff --git a/typo3/sysext/install/Classes/Service/SessionService.php b/typo3/sysext/install/Classes/Service/SessionService.php index 96b189412bda..92593cc85eae 100644 --- a/typo3/sysext/install/Classes/Service/SessionService.php +++ b/typo3/sysext/install/Classes/Service/SessionService.php @@ -14,6 +14,7 @@ * The TYPO3 project - inspiring people to share! */ +use Symfony\Component\HttpFoundation\Cookie; use TYPO3\CMS\Core\Core\Environment; use TYPO3\CMS\Core\Messaging\FlashMessage; use TYPO3\CMS\Core\SingletonInterface; @@ -77,6 +78,9 @@ public function __construct() session_save_path($sessionSavePath); session_name($this->cookieName); ini_set('session.cookie_httponly', true); + if (PHP_VERSION_ID >= 70300) { + ini_set('session.cookie_samesite', Cookie::SAMESITE_STRICT); + } ini_set('session.cookie_path', (string)GeneralUtility::getIndpEnv('TYPO3_SITE_PATH')); // Always call the garbage collector to clean up stale session files ini_set('session.gc_probability', (string)100); @@ -94,6 +98,42 @@ public function __construct() throw new \TYPO3\CMS\Install\Exception($sessionCreationError, 1294587486); } session_start(); + if (PHP_VERSION_ID < 70300) { + $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); + } + } } /**