diff --git a/CHANGELOG-4.4.md b/CHANGELOG-4.4.md
index 00b3a813dfba2..5a261d439826d 100644
--- a/CHANGELOG-4.4.md
+++ b/CHANGELOG-4.4.md
@@ -7,6 +7,11 @@ in 4.4 minor versions.
To get the diff for a specific change, go to https://github.com/symfony/symfony/commit/XXX where XXX is the change hash
To get the diff between two versions, go to https://github.com/symfony/symfony/compare/v4.4.0...v4.4.1
+* 4.4.50 (2023-02-01)
+
+ * security #cve-2022-24895 [Security/Http] Remove CSRF tokens from storage on successful login (nicolas-grekas)
+ * security #cve-2022-24894 [HttpKernel] Remove private headers before storing responses with HttpCache (nicolas-grekas)
+
* 4.4.49 (2022-11-28)
* bug #48273 [HttpKernel] Fix message for unresovable arguments of invokable controllers (fancyweb)
diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml
index 3491383b8bba6..eabe5e547fada 100644
--- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml
+++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml
@@ -63,6 +63,7 @@
%security.authentication.session_strategy.strategy%
+
diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/CsrfFormLoginTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/CsrfFormLoginTest.php
index 1a672d70f8335..08ea67a6416fa 100644
--- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/CsrfFormLoginTest.php
+++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/CsrfFormLoginTest.php
@@ -19,12 +19,15 @@ class CsrfFormLoginTest extends AbstractWebTestCase
public function testFormLoginAndLogoutWithCsrfTokens($config)
{
$client = $this->createClient(['test_case' => 'CsrfFormLogin', 'root_config' => $config]);
+ static::$container->get('security.csrf.token_storage')->setToken('foo', 'bar');
$form = $client->request('GET', '/login')->selectButton('login')->form();
$form['user_login[username]'] = 'johannes';
$form['user_login[password]'] = 'test';
$client->submit($form);
+ $this->assertFalse(static::$container->get('security.csrf.token_storage')->hasToken('foo'));
+
$this->assertRedirect($client->getResponse(), '/profile');
$crawler = $client->followRedirect();
@@ -48,11 +51,14 @@ public function testFormLoginAndLogoutWithCsrfTokens($config)
public function testFormLoginWithInvalidCsrfToken($config)
{
$client = $this->createClient(['test_case' => 'CsrfFormLogin', 'root_config' => $config]);
+ static::$container->get('security.csrf.token_storage')->setToken('foo', 'bar');
$form = $client->request('GET', '/login')->selectButton('login')->form();
$form['user_login[_token]'] = '';
$client->submit($form);
+ $this->assertTrue(static::$container->get('security.csrf.token_storage')->hasToken('foo'));
+
$this->assertRedirect($client->getResponse(), '/login');
$text = $client->followRedirect()->text(null, true);
diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/LogoutTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/LogoutTest.php
index cb7868f3256ef..465027f42f0c8 100644
--- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/LogoutTest.php
+++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/LogoutTest.php
@@ -36,15 +36,13 @@ public function testSessionLessRememberMeLogout()
public function testCsrfTokensAreClearedOnLogout()
{
$client = $this->createClient(['test_case' => 'LogoutWithoutSessionInvalidation', 'root_config' => 'config.yml']);
- static::$container->get('security.csrf.token_storage')->setToken('foo', 'bar');
$client->request('POST', '/login', [
'_username' => 'johannes',
'_password' => 'test',
]);
- $this->assertTrue(static::$container->get('security.csrf.token_storage')->hasToken('foo'));
- $this->assertSame('bar', static::$container->get('security.csrf.token_storage')->getToken('foo'));
+ static::$container->get('security.csrf.token_storage')->setToken('foo', 'bar');
$client->request('GET', '/logout');
diff --git a/src/Symfony/Bundle/SecurityBundle/composer.json b/src/Symfony/Bundle/SecurityBundle/composer.json
index 1106acfa008c6..4061646f399ff 100644
--- a/src/Symfony/Bundle/SecurityBundle/composer.json
+++ b/src/Symfony/Bundle/SecurityBundle/composer.json
@@ -25,7 +25,7 @@
"symfony/security-core": "^4.4",
"symfony/security-csrf": "^4.2|^5.0",
"symfony/security-guard": "^4.2|^5.0",
- "symfony/security-http": "^4.4.5"
+ "symfony/security-http": "^4.4.50"
},
"require-dev": {
"doctrine/annotations": "^1.10.4",
diff --git a/src/Symfony/Component/HttpKernel/HttpCache/Store.php b/src/Symfony/Component/HttpKernel/HttpCache/Store.php
index eeb7a6ef948b1..43bd7c808252c 100644
--- a/src/Symfony/Component/HttpKernel/HttpCache/Store.php
+++ b/src/Symfony/Component/HttpKernel/HttpCache/Store.php
@@ -26,19 +26,29 @@ class Store implements StoreInterface
{
protected $root;
private $keyCache;
- private $locks;
+ private $locks = [];
+ private $options;
/**
+ * Constructor.
+ *
+ * The available options are:
+ *
+ * * private_headers Set of response headers that should not be stored
+ * when a response is cached. (default: Set-Cookie)
+ *
* @throws \RuntimeException
*/
- public function __construct(string $root)
+ public function __construct(string $root, array $options = [])
{
$this->root = $root;
if (!file_exists($this->root) && !@mkdir($this->root, 0777, true) && !is_dir($this->root)) {
throw new \RuntimeException(sprintf('Unable to create the store directory (%s).', $this->root));
}
$this->keyCache = new \SplObjectStorage();
- $this->locks = [];
+ $this->options = array_merge([
+ 'private_headers' => ['Set-Cookie'],
+ ], $options);
}
/**
@@ -215,6 +225,10 @@ public function write(Request $request, Response $response)
$headers = $this->persistResponse($response);
unset($headers['age']);
+ foreach ($this->options['private_headers'] as $h) {
+ unset($headers[strtolower($h)]);
+ }
+
array_unshift($entries, [$storedEnv, $headers]);
if (!$this->save($key, serialize($entries))) {
diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php
index be36bc6346550..7064edefbe456 100644
--- a/src/Symfony/Component/HttpKernel/Kernel.php
+++ b/src/Symfony/Component/HttpKernel/Kernel.php
@@ -76,11 +76,11 @@ abstract class Kernel implements KernelInterface, RebootableInterface, Terminabl
private static $freshCache = [];
- public const VERSION = '4.4.49';
- public const VERSION_ID = 40449;
+ public const VERSION = '4.4.50';
+ public const VERSION_ID = 40450;
public const MAJOR_VERSION = 4;
public const MINOR_VERSION = 4;
- public const RELEASE_VERSION = 49;
+ public const RELEASE_VERSION = 50;
public const EXTRA_VERSION = '';
public const END_OF_MAINTENANCE = '11/2022';
diff --git a/src/Symfony/Component/HttpKernel/Tests/HttpCache/StoreTest.php b/src/Symfony/Component/HttpKernel/Tests/HttpCache/StoreTest.php
index da1f649127405..239361bc8c337 100644
--- a/src/Symfony/Component/HttpKernel/Tests/HttpCache/StoreTest.php
+++ b/src/Symfony/Component/HttpKernel/Tests/HttpCache/StoreTest.php
@@ -12,8 +12,10 @@
namespace Symfony\Component\HttpKernel\Tests\HttpCache;
use PHPUnit\Framework\TestCase;
+use Symfony\Component\HttpFoundation\Cookie;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpKernel\HttpCache\HttpCache;
use Symfony\Component\HttpKernel\HttpCache\Store;
class StoreTest extends TestCase
@@ -317,6 +319,17 @@ public function testPurgeHttpAndHttps()
$this->assertEmpty($this->getStoreMetadata($requestHttps));
}
+ public function testDoesNotStorePrivateHeaders()
+ {
+ $request = Request::create('https://example.com/foo');
+ $response = new Response('foo');
+ $response->headers->setCookie(Cookie::fromString('foo=bar'));
+
+ $this->store->write($request, $response);
+ $this->assertArrayNotHasKey('set-cookie', $this->getStoreMetadata($request)[0][1]);
+ $this->assertNotEmpty($response->headers->getCookies());
+ }
+
protected function storeSimpleEntry($path = null, $headers = [])
{
if (null === $path) {
diff --git a/src/Symfony/Component/Security/Http/Session/SessionAuthenticationStrategy.php b/src/Symfony/Component/Security/Http/Session/SessionAuthenticationStrategy.php
index a4bb88818d452..73691058d0ce6 100644
--- a/src/Symfony/Component/Security/Http/Session/SessionAuthenticationStrategy.php
+++ b/src/Symfony/Component/Security/Http/Session/SessionAuthenticationStrategy.php
@@ -13,6 +13,7 @@
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
+use Symfony\Component\Security\Csrf\TokenStorage\ClearableTokenStorageInterface;
/**
* The default session strategy implementation.
@@ -31,10 +32,15 @@ class SessionAuthenticationStrategy implements SessionAuthenticationStrategyInte
public const INVALIDATE = 'invalidate';
private $strategy;
+ private $csrfTokenStorage = null;
- public function __construct(string $strategy)
+ public function __construct(string $strategy, ClearableTokenStorageInterface $csrfTokenStorage = null)
{
$this->strategy = $strategy;
+
+ if (self::MIGRATE === $strategy) {
+ $this->csrfTokenStorage = $csrfTokenStorage;
+ }
}
/**
@@ -47,10 +53,12 @@ public function onAuthentication(Request $request, TokenInterface $token)
return;
case self::MIGRATE:
- // Note: this logic is duplicated in several authentication listeners
- // until Symfony 5.0 due to a security fix with BC compat
$request->getSession()->migrate(true);
+ if ($this->csrfTokenStorage) {
+ $this->csrfTokenStorage->clear();
+ }
+
return;
case self::INVALIDATE:
diff --git a/src/Symfony/Component/Security/Http/Tests/Session/SessionAuthenticationStrategyTest.php b/src/Symfony/Component/Security/Http/Tests/Session/SessionAuthenticationStrategyTest.php
index 010bb0edf2cfd..2051f34239144 100644
--- a/src/Symfony/Component/Security/Http/Tests/Session/SessionAuthenticationStrategyTest.php
+++ b/src/Symfony/Component/Security/Http/Tests/Session/SessionAuthenticationStrategyTest.php
@@ -14,6 +14,7 @@
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
+use Symfony\Component\Security\Csrf\TokenStorage\ClearableTokenStorageInterface;
use Symfony\Component\Security\Http\Session\SessionAuthenticationStrategy;
use Symfony\Component\Security\Http\Tests\Fixtures\TokenInterface;
@@ -57,6 +58,18 @@ public function testSessionIsInvalidated()
$strategy->onAuthentication($this->getRequest($session), $this->createMock(TokenInterface::class));
}
+ public function testCsrfTokensAreCleared()
+ {
+ $session = $this->createMock(SessionInterface::class);
+ $session->expects($this->once())->method('migrate')->with($this->equalTo(true));
+
+ $csrfStorage = $this->createMock(ClearableTokenStorageInterface::class);
+ $csrfStorage->expects($this->once())->method('clear');
+
+ $strategy = new SessionAuthenticationStrategy(SessionAuthenticationStrategy::MIGRATE, $csrfStorage);
+ $strategy->onAuthentication($this->getRequest($session), $this->createMock(TokenInterface::class));
+ }
+
private function getRequest($session = null)
{
$request = $this->createMock(Request::class);