CVE-2022-24747 (PHP, published 2022-03-09T23:15Z)
Description: value: Shopware is an open commerce platform based on the Symfony php Framework and the Vue javascript framework. Affected versions of shopware do no properly set sensitive HTTP headers to be non-cacheable. If there is an HTTP cache between the server and client then headers may be exposed via HTTP caches. This issue has been resolved in version 6.4.8.2. There are no known workarounds.
Severity: MEDIUM (CVSS v3 base score: 5.3)
Weakness: CWE-668 \u2014 Exposure of Resource to Wrong Sphere
The product exposes a resource to the wrong control sphere, providing unintended actors with inappropriate access to the resource.
Repository: https://github.com/shopware/platform
Files changed:
changelog/_unreleased/2022-03-01-fix-cache-control.md
src/Storefront/DependencyInjection/services.xml
src/Storefront/Framework/Cache/CacheResponseSubscriber.php
src/Storefront/Test/Framework/Cache/CacheResponseSubscriberTest.php
src/Storefront/Test/Framework/Routing/ResponseHeaderListenerTest.php
Commit message: NEXT-20309 - Fix cache control


Vulnerable code:
        MaintenanceModeResolver $maintenanceModeResolver
            $this->getContainer()->get(MaintenanceModeResolver::class)
            $this->getContainer()->get(MaintenanceModeResolver::class)
            $this->getContainer()->get(MaintenanceModeResolver::class)
    public function testRevalidateHeaderPresent(string $route): void
        static::assertTrue($response->headers->hasCacheControlDirective('must-revalidate'));
        static::assertTrue($response->headers->hasCacheControlDirective('no-cache'));
        static::assertSame(0, $response->getMaxAge());

Fixed code:
---
title: Fix cache control
issue: NEXT-20309
author: Soner Sayakci
author_email: s.sayakci@shopware.com
---

# Storefront
* Added `\Shopware\Storefront\Framework\Cache\CacheResponseSubscriber` to ensure `cache-control: private` is send to clients when the default PHP reverse proxy is enabled

            <argument>%storefront.reverse_proxy.enabled%</argument>
use Shopware\Core\Framework\Event\BeforeSendResponseEvent;
    private bool $reverseProxyEnabled;

        MaintenanceModeResolver $maintenanceModeResolver,
        bool $reverseProxyEnabled
        $this->reverseProxyEnabled = $reverseProxyEnabled;
            BeforeSendResponseEvent::class => 'updateCacheControlForBrowser',
    /**
     * In the default HttpCache implementation the reverse proxy cache is implemented too in PHP and triggered before the response is send to the client. We don't need to send the "real" cache-control headers to the end client (browser/cloudflare).
     * If a external reverse proxy cache is used we still need to provide the actual cache-control, so the external system can cache the system correctly and set the cache-control again to
     */
    public function updateCacheControlForBrowser(BeforeSendResponseEvent $event): void
    {
        if ($this->reverseProxyEnabled) {
            return;
        }

        $response = $event->getResponse();

        $noStore = $response->headers->getCacheControlDirective('no-store');

        // We don't want that the client will cache the website, if no reverse proxy is configured
        $response->headers->remove('cache-control');
        $response->setPrivate();

        if ($noStore) {
            $response->headers->addCacheControlDirective('no-store');
        } else {
            $response->headers->addCacheControlDirective('no-cache');
        }
    }

use Shopware\Core\Framework\Event\BeforeSendResponseEvent;
            $this->getContainer()->get(MaintenanceModeResolver::class),
            false
            $this->getContainer()->get(MaintenanceModeResolver::class),
            false
            $this->getContainer()->get(MaintenanceModeResolver::class),
            false

    /**
     * @dataProvider headerCases
     */
    public function testResponseHeaders(bool $reverseProxyEnabled, ?string $beforeHeader, string $afterHeader): void
    {
        $response = new Response();

        if ($beforeHeader) {
            $response->headers->set('cache-control', $beforeHeader);
        }

        $subscriber = new CacheResponseSubscriber(
            $this->createMock(CartService::class),
            100,
            true,
            $this->createMock(MaintenanceModeResolver::class),
            $reverseProxyEnabled
        );

        $subscriber->updateCacheControlForBrowser(new BeforeSendResponseEvent(new Request(), $response));

        static::assertSame($afterHeader, $response->headers->get('cache-control'));
    }

    public function headerCases(): iterable
    {
        yield 'no cache proxy, default response' => [
            false,
            null,
            'no-cache, private',
        ];

        yield 'no cache proxy, default response with no-store (/account)' => [
            false,
            'no-store, private',
            'no-store, private',
        ];

        // @see: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#preventing_storing
        yield 'no cache proxy, no-cache will be replaced with no-store' => [
            false,
            'no-store, no-cache, private',
            'no-store, private',
        ];

        yield 'no cache proxy, public content served as private for end client' => [
            false,
            'public, s-maxage=64000',
            'no-cache, private',
        ];

        yield 'cache proxy, cache-control is not touched' => [
            true,
            'public',
            'public',
        ];

        yield 'cache proxy, cache-control is not touched #2' => [
            true,
            'public, s-maxage=64000',
            'public, s-maxage=64000',
        ];

        yield 'cache proxy, cache-control is not touched #3' => [
            true,
            'private, no-store',
            'no-store, private', // Symfony sorts the cache-control
        ];
    }
    public function testNoStoreHeaderPresent(string $route): void
        static::assertLessThanOrEqual(0, $response->getMaxAge());