Skip to content
Snippets Groups Projects
Commit fb0b2624 authored by Oliver Hader's avatar Oliver Hader Committed by Oliver Hader
Browse files

[TASK] Streamline SameSite cookie handling

Patch for issue #90351 in master branch was merged fast.
Some aspects were missing which are streamlined with this change.

- workspace preview "ADMCMD_prev" using backend user setting
  ("strict" by default)

Resolves: #90380
Releases: master
Change-Id: I8d244db64a438d7537310787934a49abe3ebf28d
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/63256


Tested-by: default avatarTYPO3com <noreply@typo3.com>
Tested-by: default avatarOliver Hader <oliver.hader@typo3.org>
Reviewed-by: default avatarOliver Hader <oliver.hader@typo3.org>
parent 9150488f
Branches
Tags
No related merge requests found
......@@ -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'].
......
<?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;
}
}
......@@ -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();
}
......
......@@ -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);
}
/**
......
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment