CVE-2022-24894: Improper Authorization (CWE-285)
value: Symfony is a PHP framework for web and console applications and a set of reusable PHP components. The Symfony HTTP cache system, acts as a reverse proxy: It caches entire responses (including headers) and returns them to the clients. In a recent change in the `AbstractSessionListener`, the response might contain a `Set-Cookie` header. If the Symfony HTTP cache system is enabled, this response might bill stored and return to the next clients. An attacker can use this vulnerability to retrieve the victim's session. This issue has been patched and is available for branch 4.4.

CWE description: The product does not perform or incorrectly performs an authorization check when an actor attempts to access a resource or perform an action.
CVSS v3 base score: 8.8
Published: 2023-02-03T22:15Z
Repository: https://github.com/symfony/symfony
Language: PHP
Files changed: src/Symfony/Component/HttpKernel/HttpCache/Store.php, src/Symfony/Component/HttpKernel/Tests/HttpCache/StoreTest.php
Commit date: 2022-05-04 16:58:11 +0200
Commit message: [HttpKernel] Remove private headers before storing responses with HttpCache

Diff: diff --git a/src/Symfony/Component/HttpKernel/HttpCache/Store.php b/src/Symfony/Component/HttpKernel/HttpCache/Store.php
index eeb7a6ef948b..43bd7c808252 100644
--- a/src/Symfony/Component/HttpKernel/HttpCache/Store.php
+++ b/src/Symfony/Component/HttpKernel/HttpCache/Store.php
@@ -24,23 +24,33 @@ use Symfony\Component\HttpFoundation\Response;
  */
 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);
     }
 
     /**
      * Cleanups storage.
      */
@@ -213,10 +223,14 @@ class Store implements StoreInterface
         }
 
         $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))) {
             throw new \RuntimeException('Unable to store the metadata.');
         }
diff --git a/src/Symfony/Component/HttpKernel/Tests/HttpCache/StoreTest.php b/src/Symfony/Component/HttpKernel/Tests/HttpCache/StoreTest.php
index da1f64912740..239361bc8c33 100644
--- a/src/Symfony/Component/HttpKernel/Tests/HttpCache/StoreTest.php
+++ b/src/Symfony/Component/HttpKernel/Tests/HttpCache/StoreTest.php
@@ -10,12 +10,14 @@
  */
 
 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
 {
     protected $request;
@@ -315,10 +317,21 @@ class StoreTest extends TestCase
         $this->assertTrue($this->store->purge('http://example.com/foo'));
         $this->assertEmpty($this->getStoreMetadata($requestHttp));
         $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) {
             $path = '/test';
         }

Vulnerable code:     private $locks;
    public function __construct(string $root)
        $this->locks = [];
Fixed code:     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)
     *
    public function __construct(string $root, array $options = [])
        $this->options = array_merge([
            'private_headers' => ['Set-Cookie'],
        ], $options);
        foreach ($this->options['private_headers'] as $h) {
            unset($headers[strtolower($h)]);
        }

use Symfony\Component\HttpFoundation\Cookie;
use Symfony\Component\HttpKernel\HttpCache\HttpCache;
    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());
    }