CVE-2024-27917: Insufficient Information (NVD-CWE-noinfo)
value: Shopware is an open commerce platform based on Symfony Framework and Vue. The Symfony Session Handler pops the Session Cookie and assigns it to the Response. Since Shopware 6.5.8.0, the 404 pages are cached to improve the performance of 404 pages. So the cached Response which contains a Session Cookie when the Browser accessing the 404 page, has no cookies yet. The Symfony Session Handler is in use, when no explicit Session configuration has been done. When Redis is in use for Sessions using the PHP Redis extension, this exploiting code is not used. Shopware version 6.5.8.7 contains a patch for this issue. As a workaround, use Redis for Sessions, as this does not trigger the exploit code.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
Published: 2024-03-06T20:15Z
Repository: https://github.com/shopware/storefront
Language: PHP
Files changed: DependencyInjection/services.xml, Framework/Routing/NotFound/NotFoundSubscriber.php
Commit date: 2024-03-05 13:08:36 +0100
Commit message: NEXT-34113 - Clear cookies on 404 pages

Diff: diff --git a/DependencyInjection/services.xml b/DependencyInjection/services.xml
index c6f3c1007..a9b7c12f5 100644
--- a/DependencyInjection/services.xml
+++ b/DependencyInjection/services.xml
@@ -180,10 +180,11 @@
             <argument type="service" id="cache.object"/>
             <argument type="service" id="Shopware\Core\Framework\Adapter\Cache\CacheTracer"/>
             <argument type="service" id="Shopware\Core\Framework\DataAbstractionLayer\Cache\EntityCacheKeyGenerator"/>
             <argument type="service" id="Shopware\Core\Framework\Adapter\Cache\CacheInvalidator"/>
             <argument type="service" id="event_dispatcher"/>
+            <argument>%session.storage.options%</argument>
 
             <tag name="kernel.event_subscriber"/>
             <tag name="kernel.reset" method="reset"/>
         </service>
 
diff --git a/Framework/Routing/NotFound/NotFoundSubscriber.php b/Framework/Routing/NotFound/NotFoundSubscriber.php
index f67f170bb..923c67a66 100644
--- a/Framework/Routing/NotFound/NotFoundSubscriber.php
+++ b/Framework/Routing/NotFound/NotFoundSubscriber.php
@@ -40,25 +40,30 @@ class NotFoundSubscriber implements EventSubscriberInterface, ResetInterface
     /**
      * Catch the errors only once in a request cycle, otherwise we get an endless loop
      */
     private bool $handled = false;
 
+    private string $sessionName;
+
     /**
      * @internal
      *
      * @param AbstractCacheTracer<Response> $cacheTracer
+     * @param array{name?: string} $sessionOptions
      */
     public function __construct(
         private readonly HttpKernelInterface $httpKernel,
         private readonly SalesChannelContextServiceInterface $contextService,
         private bool $kernelDebug,
         private readonly CacheInterface $cache,
         private readonly AbstractCacheTracer $cacheTracer,
         private readonly EntityCacheKeyGenerator $generator,
         private readonly CacheInvalidator $cacheInvalidator,
-        private readonly EventDispatcherInterface $eventDispatcher
+        private readonly EventDispatcherInterface $eventDispatcher,
+        array $sessionOptions = []
     ) {
+        $this->sessionName = $sessionOptions['name'] ?? 'session-';
     }
 
     public static function getSubscribedEvents(): array
     {
         return [
@@ -121,10 +126,17 @@ class NotFoundSubscriber implements EventSubscriberInterface, ResetInterface
             if ($response instanceof StorefrontResponse && !Feature::isActive('v6.6.0.0')) {
                 $response->setData([]);
                 $response->setContext(null);
             }
 
+            // Remove session cookie from 404 pages, injected by the Symfony session listener
+            foreach ($response->headers->getCookies() as $cookie) {
+                if ($cookie->getName() === $this->sessionName) {
+                    $response->headers->removeCookie($cookie->getName(), $cookie->getPath(), $cookie->getDomain());
+                }
+            }
+
             return $response;
         });
 
         $event->setResponse($response);
     }
diff --git a/Test/Framework/Routing/NotFound/NotFoundPageCacheKeyEventTest.php b/Test/Framework/Routing/NotFound/NotFoundPageCacheKeyEventTest.php
deleted file mode 100644
index 10d3d976e..000000000
--- a/Test/Framework/Routing/NotFound/NotFoundPageCacheKeyEventTest.php
+++ /dev/null
@@ -1,34 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Shopware\Storefront\Test\Framework\Routing\NotFound;
-
-use PHPUnit\Framework\TestCase;
-use Shopware\Core\Framework\Context;
-use Shopware\Core\System\SalesChannel\SalesChannelContext;
-use Shopware\Storefront\Framework\Routing\NotFound\NotFoundPageCacheKeyEvent;
-use Symfony\Component\HttpFoundation\Request;
-
-/**
- * @internal
- *
- * @covers \Shopware\Storefront\Framework\Routing\NotFound\NotFoundPageCacheKeyEvent
- */
-class NotFoundPageCacheKeyEventTest extends TestCase
-{
-    public function testEvent(): void
-    {
-        $request = new Request();
-        $context = $this->createMock(SalesChannelContext::class);
-        $context->method('getContext')->willReturn(Context::createDefaultContext());
-
-        $event = new NotFoundPageCacheKeyEvent('test', $request, $context);
-
-        static::assertSame('test', $event->getKey());
-        static::assertSame($context->getContext(), $event->getContext());
-        static::assertSame($context, $event->getSalesChannelContext());
-        static::assertSame($request, $event->getRequest());
-
-        $event->setKey('test2');
-        static::assertSame('test2', $event->getKey());
-    }
-}
diff --git a/Test/Framework/Routing/NotFound/NotFoundPageTagsEventTest.php b/Test/Framework/Routing/NotFound/NotFoundPageTagsEventTest.php
deleted file mode 100644
index 74ca5c1fd..000000000
--- a/Test/Framework/Routing/NotFound/NotFoundPageTagsEventTest.php
+++ /dev/null
@@ -1,34 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Shopware\Storefront\Test\Framework\Routing\NotFound;
-
-use PHPUnit\Framework\TestCase;
-use Shopware\Core\Framework\Context;
-use Shopware\Core\System\SalesChannel\SalesChannelContext;
-use Shopware\Storefront\Framework\Routing\NotFound\NotFoundPageTagsEvent;
-use Symfony\Component\HttpFoundation\Request;
-
-/**
- * @internal
- *
- * @covers \Shopware\Storefront\Framework\Routing\NotFound\NotFoundPageTagsEvent
- */
-class NotFoundPageTagsEventTest extends TestCase
-{
-    public function testEvent(): void
-    {
-        $request = new Request();
-        $context = $this->createMock(SalesChannelContext::class);
-        $context->method('getContext')->willReturn(Context::createDefaultContext());
-
-        $event = new NotFoundPageTagsEvent(['test'], $request, $context);
-
-        static::assertSame(['test'], $event->getTags());
-        static::assertSame($context->getContext(), $event->getContext());
-        static::assertSame($context, $event->getSalesChannelContext());
-        static::assertSame($request, $event->getRequest());
-
-        $event->addTags(['test2']);
-        static::assertSame(['test', 'test2'], $event->getTags());
-    }
-}
diff --git a/Test/Framework/Routing/NotFound/NotFoundSubscriberTest.php b/Test/Framework/Routing/NotFound/NotFoundSubscriberTest.php
deleted file mode 100644
index ad8ce44cc..000000000
--- a/Test/Framework/Routing/NotFound/NotFoundSubscriberTest.php
+++ /dev/null
@@ -1,188 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Shopware\Storefront\Test\Framework\Routing\NotFound;
-
-use PHPUnit\Framework\TestCase;
-use Shopware\Core\Framework\Adapter\Cache\AbstractCacheTracer;
-use Shopware\Core\Framework\Adapter\Cache\CacheInvalidator;
-use Shopware\Core\Framework\DataAbstractionLayer\Cache\EntityCacheKeyGenerator;
-use Shopware\Core\Kernel;
-use Shopware\Core\System\SalesChannel\Context\SalesChannelContextServiceInterface;
-use Shopware\Core\System\SystemConfig\Event\SystemConfigChangedEvent;
-use Shopware\Storefront\Framework\Routing\NotFound\NotFoundSubscriber;
-use Shopware\Storefront\Framework\Routing\StorefrontResponse;
-use Symfony\Component\Cache\Adapter\ArrayAdapter;
-use Symfony\Component\Cache\Adapter\TagAwareAdapter;
-use Symfony\Component\EventDispatcher\EventDispatcher;
-use Symfony\Component\HttpFoundation\Request;
-use Symfony\Component\HttpFoundation\RequestStack;
-use Symfony\Component\HttpFoundation\Response;
-use Symfony\Component\HttpKernel\Event\ExceptionEvent;
-use Symfony\Component\HttpKernel\Exception\HttpException;
-use Symfony\Component\HttpKernel\HttpKernelInterface;
-use Symfony\Component\HttpKernel\KernelEvents;
-use Symfony\Contracts\Cache\CacheInterface;
-
-/**
- * @internal
- *
- * @covers \Shopware\Storefront\Framework\Routing\NotFound\NotFoundSubscriber
- */
-class NotFoundSubscriberTest extends TestCase
-{
-    public function testDebugIsOnDoesNothing(): void
-    {
-        $subscriber = new NotFoundSubscriber(
-            $this->createMock(HttpKernelInterface::class),
-            $this->createMock(SalesChannelContextServiceInterface::class),
-            true,
-            $this->createMock(CacheInterface::class),
-            $this->createMock(AbstractCacheTracer::class),
-            $this->createMock(EntityCacheKeyGenerator::class),
-            $this->createMock(CacheInvalidator::class),
-            new EventDispatcher()
-        );
-
-        $event = new ExceptionEvent(
-            $this->createMock(Kernel::class),
-            new Request(),
-            0,
-            new \Exception()
-        );
-        $subscriber->onError($event);
-
-        static::assertNull($event->getResponse());
-    }
-
-    public function testErrorHandled(): void
-    {
-        $httpKernel = $this->createMock(HttpKernelInterface::class);
-        $httpKernel
-            ->expects(static::once())
-            ->method('handle')
-            ->willReturn(new StorefrontResponse());
-
-        $cacheTracer = $this->createMock(AbstractCacheTracer::class);
-        $cacheTracer
-            ->expects(static::once())
-            ->method('trace')
-            ->willReturnCallback(fn (string $name, \Closure $closure) => $closure());
-
-        $requestStack = $this->createMock(RequestStack::class);
-        $requestStack->method('getMainRequest')->willReturn(new Request());
-
-        $subscriber = new NotFoundSubscriber(
-            $httpKernel,
-            $this->createMock(SalesChannelContextServiceInterface::class),
-            false,
-            new TagAwareAdapter(new ArrayAdapter(), new ArrayAdapter()),
-            $cacheTracer,
-            $this->createMock(EntityCacheKeyGenerator::class),
-            $this->createMock(CacheInvalidator::class),
-            new EventDispatcher()
-        );
-
-        $request = new Request();
-
-        $event = new ExceptionEvent(
-            $this->createMock(Kernel::class),
-            $request,
-            0,
-            new HttpException(Response::HTTP_NOT_FOUND)
-        );
-        $subscriber->onError($event);
-
-        $response = $event->getResponse();
-
-        static::assertInstanceOf(Response::class, $response);
-    }
-
-    public function testOtherExceptionsDoesNotGetCached(): void
-    {
-        $httpKernel = $this->createMock(HttpKernelInterface::class);
-        $httpKernel
-            ->expects(static::once())
-            ->method('handle')
-            ->willReturn(new StorefrontResponse());
-
-        $cacheTracer = $this->createMock(AbstractCacheTracer::class);
-        $cacheTracer
-            ->expects(static::never())
-            ->method('trace');
-
-        $requestStack = $this->createMock(RequestStack::class);
-        $requestStack->method('getMainRequest')->willReturn(new Request());
-
-        $subscriber = new NotFoundSubscriber(
-            $httpKernel,
-            $this->createMock(SalesChannelContextServiceInterface::class),
-            false,
-            new TagAwareAdapter(new ArrayAdapter(), new ArrayAdapter()),
-            $cacheTracer,
-            $this->createMock(EntityCacheKeyGenerator::class),
-            $this->createMock(CacheInvalidator::class),
-            new EventDispatcher()
-        );
-
-        $request = new Request();
-
-        $event = new ExceptionEvent(
-            $this->createMock(Kernel::class),
-            $request,
-            0,
-            new \Exception()
-        );
-        $subscriber->onError($event);
-
-        static::assertInstanceOf(Response::class, $event->getResponse());
-
-        $subscriber->reset();
-    }
-
-    /**
-     * @dataProvider providerSystemConfigKeys
-     */
-    public function testInvalidationHappensOnSystemConfigChange(string $key, bool $shouldInvalidate): void
-    {
-        $cacheInvalidator = $this->createMock(CacheInvalidator::class);
-        $cacheInvalidator
-            ->expects($shouldInvalidate ? static::once() : static::never())
-            ->method('invalidate');
-
-        $subscriber = new NotFoundSubscriber(
-            $this->createMock(HttpKernelInterface::class),
-            $this->createMock(SalesChannelContextServiceInterface::class),
-            true,
-            $this->createMock(CacheInterface::class),
-            $this->createMock(AbstractCacheTracer::class),
-            $this->createMock(EntityCacheKeyGenerator::class),
-            $cacheInvalidator,
-            new EventDispatcher()
-        );
-
-        $subscriber->onSystemConfigChanged(new SystemConfigChangedEvent($key, 'foo', null));
-    }
-
-    /**
-     * @return iterable<string, array<mixed>>
-     */
-    public static function providerSystemConfigKeys(): iterable
-    {
-        yield 'key matches' => [
-            'core.basicInformation.http404Page',
-            true,
-        ];
-
-        yield 'key not matches' => [
-            'core.http404Page',
-            false,
-        ];
-    }
-
-    public function testSubscribedEvents(): void
-    {
-        static::assertArrayHasKey(SystemConfigChangedEvent::class, NotFoundSubscriber::getSubscribedEvents());
-
-        static::assertArrayHasKey(KernelEvents::EXCEPTION, NotFoundSubscriber::getSubscribedEvents());
-    }
-}

Vulnerable code:         private readonly EventDispatcherInterface $eventDispatcher
<?php declare(strict_types=1);

namespace Shopware\Storefront\Test\Framework\Routing\NotFound;

use PHPUnit\Framework\TestCase;
use Shopware\Core\Framework\Context;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
use Shopware\Storefront\Framework\Routing\NotFound\NotFoundPageCacheKeyEvent;
use Symfony\Component\HttpFoundation\Request;

/**
 * @internal
 *
 * @covers \Shopware\Storefront\Framework\Routing\NotFound\NotFoundPageCacheKeyEvent
 */
class NotFoundPageCacheKeyEventTest extends TestCase
{
    public function testEvent(): void
    {
        $request = new Request();
        $context = $this->createMock(SalesChannelContext::class);
        $context->method('getContext')->willReturn(Context::createDefaultContext());

        $event = new NotFoundPageCacheKeyEvent('test', $request, $context);

        static::assertSame('test', $event->getKey());
        static::assertSame($context->getContext(), $event->getContext());
        static::assertSame($context, $event->getSalesChannelContext());
        static::assertSame($request, $event->getRequest());

        $event->setKey('test2');
        static::assertSame('test2', $event->getKey());
    }
}
<?php declare(strict_types=1);

namespace Shopware\Storefront\Test\Framework\Routing\NotFound;

use PHPUnit\Framework\TestCase;
use Shopware\Core\Framework\Context;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
use Shopware\Storefront\Framework\Routing\NotFound\NotFoundPageTagsEvent;
use Symfony\Component\HttpFoundation\Request;

/**
 * @internal
 *
 * @covers \Shopware\Storefront\Framework\Routing\NotFound\NotFoundPageTagsEvent
 */
class NotFoundPageTagsEventTest extends TestCase
{
    public function testEvent(): void
    {
        $request = new Request();
        $context = $this->createMock(SalesChannelContext::class);
        $context->method('getContext')->willReturn(Context::createDefaultContext());

        $event = new NotFoundPageTagsEvent(['test'], $request, $context);

        static::assertSame(['test'], $event->getTags());
        static::assertSame($context->getContext(), $event->getContext());
        static::assertSame($context, $event->getSalesChannelContext());
        static::assertSame($request, $event->getRequest());

        $event->addTags(['test2']);
        static::assertSame(['test', 'test2'], $event->getTags());
    }
}
<?php declare(strict_types=1);

namespace Shopware\Storefront\Test\Framework\Routing\NotFound;

use PHPUnit\Framework\TestCase;
use Shopware\Core\Framework\Adapter\Cache\AbstractCacheTracer;
use Shopware\Core\Framework\Adapter\Cache\CacheInvalidator;
use Shopware\Core\Framework\DataAbstractionLayer\Cache\EntityCacheKeyGenerator;
use Shopware\Core\Kernel;
use Shopware\Core\System\SalesChannel\Context\SalesChannelContextServiceInterface;
use Shopware\Core\System\SystemConfig\Event\SystemConfigChangedEvent;
use Shopware\Storefront\Framework\Routing\NotFound\NotFoundSubscriber;
use Shopware\Storefront\Framework\Routing\StorefrontResponse;
use Symfony\Component\Cache\Adapter\ArrayAdapter;
use Symfony\Component\Cache\Adapter\TagAwareAdapter;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Contracts\Cache\CacheInterface;

/**
 * @internal
 *
 * @covers \Shopware\Storefront\Framework\Routing\NotFound\NotFoundSubscriber
 */
class NotFoundSubscriberTest extends TestCase
{
    public function testDebugIsOnDoesNothing(): void
    {
        $subscriber = new NotFoundSubscriber(
            $this->createMock(HttpKernelInterface::class),
            $this->createMock(SalesChannelContextServiceInterface::class),
            true,
            $this->createMock(CacheInterface::class),
            $this->createMock(AbstractCacheTracer::class),
            $this->createMock(EntityCacheKeyGenerator::class),
            $this->createMock(CacheInvalidator::class),
            new EventDispatcher()
        );

        $event = new ExceptionEvent(
            $this->createMock(Kernel::class),
            new Request(),
            0,
            new \Exception()
        );
        $subscriber->onError($event);

        static::assertNull($event->getResponse());
    }

    public function testErrorHandled(): void
    {
        $httpKernel = $this->createMock(HttpKernelInterface::class);
        $httpKernel
            ->expects(static::once())
            ->method('handle')
            ->willReturn(new StorefrontResponse());

        $cacheTracer = $this->createMock(AbstractCacheTracer::class);
        $cacheTracer
            ->expects(static::once())
            ->method('trace')
            ->willReturnCallback(fn (string $name, \Closure $closure) => $closure());

        $requestStack = $this->createMock(RequestStack::class);
        $requestStack->method('getMainRequest')->willReturn(new Request());

        $subscriber = new NotFoundSubscriber(
            $httpKernel,
            $this->createMock(SalesChannelContextServiceInterface::class),
            false,
            new TagAwareAdapter(new ArrayAdapter(), new ArrayAdapter()),
            $cacheTracer,
            $this->createMock(EntityCacheKeyGenerator::class),
            $this->createMock(CacheInvalidator::class),
            new EventDispatcher()
        );

        $request = new Request();

        $event = new ExceptionEvent(
            $this->createMock(Kernel::class),
            $request,
            0,
            new HttpException(Response::HTTP_NOT_FOUND)
        );
        $subscriber->onError($event);

        $response = $event->getResponse();

        static::assertInstanceOf(Response::class, $response);
    }

    public function testOtherExceptionsDoesNotGetCached(): void
    {
        $httpKernel = $this->createMock(HttpKernelInterface::class);
        $httpKernel
            ->expects(static::once())
            ->method('handle')
            ->willReturn(new StorefrontResponse());

        $cacheTracer = $this->createMock(AbstractCacheTracer::class);
        $cacheTracer
            ->expects(static::never())
            ->method('trace');

        $requestStack = $this->createMock(RequestStack::class);
        $requestStack->method('getMainRequest')->willReturn(new Request());

        $subscriber = new NotFoundSubscriber(
            $httpKernel,
            $this->createMock(SalesChannelContextServiceInterface::class),
            false,
            new TagAwareAdapter(new ArrayAdapter(), new ArrayAdapter()),
            $cacheTracer,
            $this->createMock(EntityCacheKeyGenerator::class),
            $this->createMock(CacheInvalidator::class),
            new EventDispatcher()
        );

        $request = new Request();

        $event = new ExceptionEvent(
            $this->createMock(Kernel::class),
            $request,
            0,
            new \Exception()
        );
        $subscriber->onError($event);

        static::assertInstanceOf(Response::class, $event->getResponse());

        $subscriber->reset();
    }

    /**
     * @dataProvider providerSystemConfigKeys
     */
    public function testInvalidationHappensOnSystemConfigChange(string $key, bool $shouldInvalidate): void
    {
        $cacheInvalidator = $this->createMock(CacheInvalidator::class);
        $cacheInvalidator
            ->expects($shouldInvalidate ? static::once() : static::never())
            ->method('invalidate');

        $subscriber = new NotFoundSubscriber(
            $this->createMock(HttpKernelInterface::class),
            $this->createMock(SalesChannelContextServiceInterface::class),
            true,
            $this->createMock(CacheInterface::class),
            $this->createMock(AbstractCacheTracer::class),
            $this->createMock(EntityCacheKeyGenerator::class),
            $cacheInvalidator,
            new EventDispatcher()
        );

        $subscriber->onSystemConfigChanged(new SystemConfigChangedEvent($key, 'foo', null));
    }

    /**
     * @return iterable<string, array<mixed>>
     */
    public static function providerSystemConfigKeys(): iterable
    {
        yield 'key matches' => [
            'core.basicInformation.http404Page',
            true,
        ];

        yield 'key not matches' => [
            'core.http404Page',
            false,
        ];
    }

    public function testSubscribedEvents(): void
    {
        static::assertArrayHasKey(SystemConfigChangedEvent::class, NotFoundSubscriber::getSubscribedEvents());

        static::assertArrayHasKey(KernelEvents::EXCEPTION, NotFoundSubscriber::getSubscribedEvents());
    }
}
Fixed code:             <argument>%session.storage.options%</argument>
    private string $sessionName;

     * @param array{name?: string} $sessionOptions
        private readonly EventDispatcherInterface $eventDispatcher,
        array $sessionOptions = []
        $this->sessionName = $sessionOptions['name'] ?? 'session-';
            // Remove session cookie from 404 pages, injected by the Symfony session listener
            foreach ($response->headers->getCookies() as $cookie) {
                if ($cookie->getName() === $this->sessionName) {
                    $response->headers->removeCookie($cookie->getName(), $cookie->getPath(), $cookie->getDomain());
                }
            }