diff --git a/composer.json b/composer.json
index 3b07fef484df..5b219bf535c5 100644
--- a/composer.json
+++ b/composer.json
@@ -48,6 +48,7 @@
"swiftmailer/swiftmailer": "~5.4.5",
"symfony/console": "^2.7 || ^3.0 || ^4.0",
"symfony/finder": "^2.7 || ^3.0 || ^4.0",
+ "symfony/http-foundation": "^3.4 || ^4.2",
"symfony/polyfill-mbstring": "^1.2",
"symfony/yaml": "^2.7 || ^3.0 || ^4.0",
"typo3/class-alias-loader": "^1.0",
diff --git a/composer.lock b/composer.lock
index 09c13a9c2bcb..5c91e6b3966e 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": "443f9b14d81e5a20ee0ca8badbadcc59",
+ "content-hash": "58891391ba9f5205e4572c948c4bdfb4",
"packages": [
{
"name": "algo26-matthias/idna-convert",
@@ -813,6 +813,51 @@
],
"time": "2018-12-04T20:46:45+00:00"
},
+ {
+ "name": "paragonie/random_compat",
+ "version": "v9.99.99",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/paragonie/random_compat.git",
+ "reference": "84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/paragonie/random_compat/zipball/84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95",
+ "reference": "84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "4.*|5.*",
+ "vimeo/psalm": "^1"
+ },
+ "suggest": {
+ "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes."
+ },
+ "type": "library",
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Paragon Initiative Enterprises",
+ "email": "security@paragonie.com",
+ "homepage": "https://paragonie.com"
+ }
+ ],
+ "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7",
+ "keywords": [
+ "csprng",
+ "polyfill",
+ "pseudorandom",
+ "random"
+ ],
+ "time": "2018-07-02T15:55:56+00:00"
+ },
{
"name": "psr/http-message",
"version": "1.0.1",
@@ -1181,6 +1226,60 @@
"homepage": "https://symfony.com",
"time": "2019-11-17T21:55:15+00:00"
},
+ {
+ "name": "symfony/http-foundation",
+ "version": "v3.4.37",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/http-foundation.git",
+ "reference": "f3abd07a56111ebe6a1ad6f1cbc23e4f8983f8f5"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/http-foundation/zipball/f3abd07a56111ebe6a1ad6f1cbc23e4f8983f8f5",
+ "reference": "f3abd07a56111ebe6a1ad6f1cbc23e4f8983f8f5",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.5.9|>=7.0.8",
+ "symfony/polyfill-mbstring": "~1.1",
+ "symfony/polyfill-php70": "~1.6"
+ },
+ "require-dev": {
+ "symfony/expression-language": "~2.8|~3.0|~4.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.4-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": "2020-01-04T12:05:51+00:00"
+ },
{
"name": "symfony/polyfill-ctype",
"version": "v1.13.1",
@@ -1298,6 +1397,65 @@
],
"time": "2019-11-27T14:18:11+00:00"
},
+ {
+ "name": "symfony/polyfill-php70",
+ "version": "v1.13.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-php70.git",
+ "reference": "af23c7bb26a73b850840823662dda371484926c4"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-php70/zipball/af23c7bb26a73b850840823662dda371484926c4",
+ "reference": "af23c7bb26a73b850840823662dda371484926c4",
+ "shasum": ""
+ },
+ "require": {
+ "paragonie/random_compat": "~1.0|~2.0|~9.99",
+ "php": ">=5.3.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.13-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Polyfill\\Php70\\": ""
+ },
+ "files": [
+ "bootstrap.php"
+ ],
+ "classmap": [
+ "Resources/stubs"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill backporting some PHP 7.0+ features to lower PHP versions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "time": "2019-11-27T13:56:44+00:00"
+ },
{
"name": "symfony/yaml",
"version": "v3.4.36",
@@ -1989,6 +2147,7 @@
"selenium",
"webdriver"
],
+ "abandoned": "php-webdriver/webdriver",
"time": "2017-01-13T15:48:08+00:00"
},
{
@@ -2264,51 +2423,6 @@
],
"time": "2017-04-12T18:52:22+00:00"
},
- {
- "name": "paragonie/random_compat",
- "version": "v9.99.99",
- "source": {
- "type": "git",
- "url": "https://github.com/paragonie/random_compat.git",
- "reference": "84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/paragonie/random_compat/zipball/84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95",
- "reference": "84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95",
- "shasum": ""
- },
- "require": {
- "php": "^7"
- },
- "require-dev": {
- "phpunit/phpunit": "4.*|5.*",
- "vimeo/psalm": "^1"
- },
- "suggest": {
- "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes."
- },
- "type": "library",
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Paragon Initiative Enterprises",
- "email": "security@paragonie.com",
- "homepage": "https://paragonie.com"
- }
- ],
- "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7",
- "keywords": [
- "csprng",
- "polyfill",
- "pseudorandom",
- "random"
- ],
- "time": "2018-07-02T15:55:56+00:00"
- },
{
"name": "phar-io/manifest",
"version": "1.0.1",
@@ -4002,65 +4116,6 @@
],
"time": "2019-10-26T11:02:01+00:00"
},
- {
- "name": "symfony/polyfill-php70",
- "version": "v1.13.1",
- "source": {
- "type": "git",
- "url": "https://github.com/symfony/polyfill-php70.git",
- "reference": "af23c7bb26a73b850840823662dda371484926c4"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-php70/zipball/af23c7bb26a73b850840823662dda371484926c4",
- "reference": "af23c7bb26a73b850840823662dda371484926c4",
- "shasum": ""
- },
- "require": {
- "paragonie/random_compat": "~1.0|~2.0|~9.99",
- "php": ">=5.3.3"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "1.13-dev"
- }
- },
- "autoload": {
- "psr-4": {
- "Symfony\\Polyfill\\Php70\\": ""
- },
- "files": [
- "bootstrap.php"
- ],
- "classmap": [
- "Resources/stubs"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Nicolas Grekas",
- "email": "p@tchwork.com"
- },
- {
- "name": "Symfony Community",
- "homepage": "https://symfony.com/contributors"
- }
- ],
- "description": "Symfony polyfill backporting some PHP 7.0+ features to lower PHP versions",
- "homepage": "https://symfony.com",
- "keywords": [
- "compatibility",
- "polyfill",
- "portable",
- "shim"
- ],
- "time": "2019-11-27T13:56:44+00:00"
- },
{
"name": "symfony/polyfill-php72",
"version": "v1.13.1",
diff --git a/typo3/sysext/backend/Classes/Controller/LoginController.php b/typo3/sysext/backend/Classes/Controller/LoginController.php
index fb682dad5f08..13adbfddf14b 100644
--- a/typo3/sysext/backend/Classes/Controller/LoginController.php
+++ b/typo3/sysext/backend/Classes/Controller/LoginController.php
@@ -16,6 +16,7 @@
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
+use Symfony\Component\HttpFoundation\Cookie;
use TYPO3\CMS\Backend\Exception;
use TYPO3\CMS\Backend\LoginProvider\LoginProviderInterface;
use TYPO3\CMS\Backend\Utility\BackendUtility;
@@ -91,8 +92,6 @@ public function __construct()
{
$this->validateAndSortLoginProviders();
- // We need a PHP session session for most login levels
- session_start();
$this->redirectUrl = GeneralUtility::sanitizeLocalUrl(GeneralUtility::_GP('redirect_url'));
$this->loginProviderIdentifier = $this->detectLoginProvider();
@@ -492,7 +491,19 @@ protected function detectLoginProvider()
}
// Use the secure option when the current request is served by a secure connection:
$cookieSecure = (bool)$GLOBALS['TYPO3_CONF_VARS']['SYS']['cookieSecure'] && GeneralUtility::getIndpEnv('TYPO3_SSL');
- setcookie('be_lastLoginProvider', $loginProvider, $GLOBALS['EXEC_TIME'] + 7776000, null, null, $cookieSecure, true); // 90 days
+ $cookie = new Cookie(
+ 'be_lastLoginProvider',
+ (string)$loginProvider,
+ $GLOBALS['EXEC_TIME'] + 7776000, // 90 days
+ GeneralUtility::getIndpEnv('TYPO3_SITE_PATH') . TYPO3_mainDir,
+ '',
+ $cookieSecure,
+ true,
+ false,
+ Cookie::SAMESITE_STRICT
+ );
+ header('Set-Cookie: ' . $cookie->__toString(), false);
+
return $loginProvider;
}
diff --git a/typo3/sysext/core/Classes/Authentication/AbstractUserAuthentication.php b/typo3/sysext/core/Classes/Authentication/AbstractUserAuthentication.php
index 2cdee16084de..95caade59b45 100644
--- a/typo3/sysext/core/Classes/Authentication/AbstractUserAuthentication.php
+++ b/typo3/sysext/core/Classes/Authentication/AbstractUserAuthentication.php
@@ -14,6 +14,7 @@
* The TYPO3 project - inspiring people to share!
*/
+use Symfony\Component\HttpFoundation\Cookie;
use TYPO3\CMS\Core\Crypto\Random;
use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;
@@ -26,6 +27,7 @@
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;
@@ -44,6 +46,8 @@
*/
abstract class AbstractUserAuthentication
{
+ use CookieHeaderTrait;
+
/**
* Session/Cookie name
* @var string
@@ -483,9 +487,28 @@ 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');
+ // 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;
+ }
// 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);
diff --git a/typo3/sysext/core/Classes/Http/CookieHeaderTrait.php b/typo3/sysext/core/Classes/Http/CookieHeaderTrait.php
new file mode 100644
index 000000000000..da00e29d6799
--- /dev/null
+++ b/typo3/sysext/core/Classes/Http/CookieHeaderTrait.php
@@ -0,0 +1,75 @@
+=');
+ }
+
+ /**
+ * 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 = [])
+ {
+ $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;
+ }
+ $cookie = new Cookie(
+ $cookie->getName(),
+ $cookie->getValue(),
+ $cookie->getExpiresTime(),
+ $cookie->getPath(),
+ $cookie->getDomain(),
+ $cookie->isSecure(),
+ $cookie->isHttpOnly(),
+ $cookie->isRaw(),
+ $sameSite
+ );
+ return (string)$cookie;
+ }, $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/core/Configuration/DefaultConfiguration.php b/typo3/sysext/core/Configuration/DefaultConfiguration.php
index ad8898ab2f73..4d3d98a7b0c7 100644
--- a/typo3/sysext/core/Configuration/DefaultConfiguration.php
+++ b/typo3/sysext/core/Configuration/DefaultConfiguration.php
@@ -867,6 +867,7 @@
'enabledBeUserIPLock' => true,
'cookieDomain' => '',
'cookieName' => 'be_typo_user',
+ 'cookieSameSite' => 'strict',
'loginSecurityLevel' => '',
'showRefreshLoginPopup' => false,
'adminOnly' => 0,
@@ -996,6 +997,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.php b/typo3/sysext/core/Configuration/DefaultConfigurationDescription.php
index fc3c6d9d51a3..19b4b8a69605 100644
--- a/typo3/sysext/core/Configuration/DefaultConfigurationDescription.php
+++ b/typo3/sysext/core/Configuration/DefaultConfigurationDescription.php
@@ -113,6 +113,7 @@
'enabledBeUserIPLock' => 'Boolean: If set, the User/Group TSconfig option option.lockToIP
is enabled.',
'cookieDomain' => 'Same as $TYPO3_CONF_VARS[\'SYS\'][\'cookieDomain\'] but only for BE cookies. If empty, $TYPO3_CONF_VARS[\'SYS\'][\'cookieDomain\'] value will be used.',
'cookieName' => 'String: Set the name for the cookie used for the back-end user session',
+ 'cookieSameSite' => 'Indicates that the cookie should send proper information where the cookie can be shared (first-party cookies vs. third-party cookies) in TYPO3 Backend. Allowed values are "strict", "lax" or "none"',
'loginSecurityLevel' => 'String: Keywords that determines the security level of login to the backend. "normal" means the password from the login form is sent in clear-text, "rsa" uses RSA password encryption (only if the rsaauth extension is installed).',
'showRefreshLoginPopup' => 'Boolean: If set, the Ajax relogin will show a real popup window for relogin after the count down. Some auth services need this as they add custom validation to the login form. If it\'s not set, the Ajax relogin will show an inline relogin window.',
'adminOnly' => '
Integer (-1, 0, 1, 2)