[FEATURE] Implement SameSite option for TYPO3 cookies 83/63183/9
authorBenni Mack <benni@typo3.org>
Mon, 10 Feb 2020 06:50:35 +0000 (07:50 +0100)
committerGeorg Ringer <georg.ringer@gmail.com>
Thu, 13 Feb 2020 09:36:27 +0000 (10:36 +0100)
This change introduces a new security option for setting the SameSite
option to all cookies sent by TYPO3 Core.

Namely:
- Frontend User Sessions ("lax" by default)
- Backend User Sessions ("strict" by default)
- Install Tool Sessions ("strict", none-configurable)
- Last Login Provider in Backend ("strict", non-configurable)

This means that these can only be accessed by scripts and requests
by the same site, and not by any third-party scripts.

Since we're talking about actual cookies for a user, and not
ads-related or third-party login-dependant cookies, the default
options fit just perfectly.

All modern browsers except Internet Explorer respect this option
to be set. Please note that Firefox and Chrome will have "SameSite=lax"
set in Q1/2020 by default if NO SameSite option is set at all. This change
allows to configure this.

Backend and Frontend User Cookies can be configured to "strict", "lax"
or "none" (= same as before), whereas "none" only works for secure
connections (= HTTPS).

If "strict" is in place, security via CSRF is not needed anymore, and can
be dropped in the future.

Resolves: #90351
Releases: master, 9.5, 8.7
Change-Id: I8095e2a552faa9d1fd4fa7855297302a9ec6a75f
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/63183
Tested-by: Anja Leichsenring <aleichsenring@ab-softlab.de>
Tested-by: TYPO3com <noreply@typo3.com>
Tested-by: Georg Ringer <georg.ringer@gmail.com>
Reviewed-by: Anja Leichsenring <aleichsenring@ab-softlab.de>
Reviewed-by: Georg Ringer <georg.ringer@gmail.com>
composer.json
composer.lock
typo3/sysext/backend/Classes/Controller/LoginController.php
typo3/sysext/core/Classes/Authentication/AbstractUserAuthentication.php
typo3/sysext/core/Configuration/DefaultConfiguration.php
typo3/sysext/core/Configuration/DefaultConfigurationDescription.yaml
typo3/sysext/core/Documentation/Changelog/8.7.x/Feature-90351-ConfigureTYPO3-shippedCookiesWithSameSiteFlag.rst [new file with mode: 0644]
typo3/sysext/core/composer.json
typo3/sysext/install/Classes/Service/SessionService.php

index aabd8e7..db95bc5 100644 (file)
@@ -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",
index 9f72809..9cf2eb8 100644 (file)
@@ -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",
             "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",
             "source": {
             "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",
             "source": {
index 440c20e..ccb52ad 100644 (file)
@@ -20,6 +20,7 @@ use Psr\Http\Message\ResponseInterface;
 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\Database\ConnectionPool;
 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 @@ class LoginController implements LoggerAwareInterface
             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;
     }
 
index b75abf1..ecc4e51 100644 (file)
@@ -16,6 +16,7 @@ namespace TYPO3\CMS\Core\Authentication;
 
 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 @@ 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)
+            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);
@@ -466,6 +483,24 @@ 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'].
      *
index 891b0f9..5366f2b 100644 (file)
@@ -1082,6 +1082,7 @@ return [
         'enabledBeUserIPLock' => true,
         'cookieDomain' => '',
         'cookieName' => 'be_typo_user',
+        'cookieSameSite' => 'strict',
         'loginSecurityLevel' => 'normal',
         'showRefreshLoginPopup' => false,
         'adminOnly' => 0,
@@ -1262,6 +1263,7 @@ return [
         '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)
index 7ca838d..8b07f1e 100644 (file)
@@ -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 (file)
index 0000000..0608816
--- /dev/null
@@ -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
index c9eaddd..214ae42 100644 (file)
@@ -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",
index 96b1894..92593cc 100644 (file)
@@ -14,6 +14,7 @@ namespace TYPO3\CMS\Install\Service;
  * 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 @@ class SessionService implements SingletonInterface
         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 @@ class SessionService implements SingletonInterface
             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);
+            }
+        }
     }
 
     /**