CVE-2022-24872: Incorrect Permission Assignment for Critical Resource (CWE-732)
value: Shopware is an open commerce platform based on Symfony Framework and Vue. Permissions set to sales channel context by admin-api are still usable within normal user session. Users are advised to update to the current version 6.4.10.1. For older versions of 6.1, 6.2, and 6.3, corresponding security measures are also available via a plugin. There are no known workarounds for this issue.
CWE description: The product specifies permissions for a security-critical resource in a way that allows that resource to be read or modified by unintended actors.
CVSS v3 base score: 8.1
Published: 2022-04-20T20:15Z
Repository: https://github.com/shopware/platform
Language: PHP
Files changed: src/Core/Framework/Api/Controller/SalesChannelProxyController.php, src/Core/System/SalesChannel/Context/CartRestorer.php, src/Core/System/SalesChannel/Context/SalesChannelContextPersister.php, src/Core/System/Test/SalesChannel/Context/CartRestorerTest.php
Commit date: 2022-04-12 11:58:45 +0200
Commit message: NEXT-21034 - Dont restore permissions

Diff: diff --git a/src/Core/Framework/Api/Controller/SalesChannelProxyController.php b/src/Core/Framework/Api/Controller/SalesChannelProxyController.php
index 049eca71a05..0110b1573d3 100644
--- a/src/Core/Framework/Api/Controller/SalesChannelProxyController.php
+++ b/src/Core/Framework/Api/Controller/SalesChannelProxyController.php
@@ -196,10 +196,11 @@ class SalesChannelProxyController extends AbstractController
 
         $content = json_encode([
             PlatformRequest::HEADER_CONTEXT_TOKEN => $salesChannelContext->getToken(),
         ], \JSON_THROW_ON_ERROR);
         $response = new Response();
+        $response->headers->set('content-type', 'application/json');
         $response->setContent($content ?: null);
 
         return $response;
     }
 
diff --git a/src/Core/System/SalesChannel/Context/CartRestorer.php b/src/Core/System/SalesChannel/Context/CartRestorer.php
index fcb335572b9..a69781110e8 100644
--- a/src/Core/System/SalesChannel/Context/CartRestorer.php
+++ b/src/Core/System/SalesChannel/Context/CartRestorer.php
@@ -44,11 +44,11 @@ class CartRestorer
             $currentContext->getToken(),
             $currentContext->getSalesChannel()->getId(),
             $customerId
         );
 
-        if (empty($customerPayload) || !($customerPayload['expired'] ?? false) && $customerPayload['token'] === $currentContext->getToken()) {
+        if (empty($customerPayload) || !empty($customerPayload['permissions']) || !($customerPayload['expired'] ?? false) && $customerPayload['token'] === $currentContext->getToken()) {
             return $this->replaceContextToken($customerId, $currentContext);
         }
 
         $customerContext = $this->factory->create($customerPayload['token'], $currentContext->getSalesChannel()->getId(), $customerPayload);
         if ($customerPayload['expired'] ?? false) {
@@ -112,10 +112,11 @@ class CartRestorer
             $newToken,
             [
                 'customerId' => $customerId,
                 'billingAddressId' => null,
                 'shippingAddressId' => null,
+                'permissions' => [],
             ],
             $currentContext->getSalesChannel()->getId(),
             $customerId
         );
 
diff --git a/src/Core/System/SalesChannel/Context/SalesChannelContextPersister.php b/src/Core/System/SalesChannel/Context/SalesChannelContextPersister.php
index 1205f87aff6..1e2f2469a0a 100644
--- a/src/Core/System/SalesChannel/Context/SalesChannelContextPersister.php
+++ b/src/Core/System/SalesChannel/Context/SalesChannelContextPersister.php
@@ -30,15 +30,18 @@ class SalesChannelContextPersister
         $this->connection = $connection;
         $this->eventDispatcher = $eventDispatcher;
         $this->lifetimeInterval = $lifetimeInterval ?? 'P1D';
     }
 
-    public function save(string $token, array $parameters, string $salesChannelId, ?string $customerId = null): void
+    public function save(string $token, array $newParameters, string $salesChannelId, ?string $customerId = null): void
     {
         $existing = $this->load($token, $salesChannelId, $customerId);
 
-        $parameters = array_replace_recursive($existing, $parameters);
+        $parameters = array_replace_recursive($existing, $newParameters);
+        if (isset($newParameters['permissions']) && $newParameters['permissions'] === []) {
+            $parameters['permissions'] = [];
+        }
 
         unset($parameters['token']);
 
         $this->connection->executeUpdate(
             'REPLACE INTO sales_channel_api_context (`token`, `payload`, `sales_channel_id`, `customer_id`, `updated_at`)
diff --git a/src/Core/System/Test/SalesChannel/Context/CartRestorerTest.php b/src/Core/System/Test/SalesChannel/Context/CartRestorerTest.php
index ff66d0b4aa5..57b9857d00e 100644
--- a/src/Core/System/Test/SalesChannel/Context/CartRestorerTest.php
+++ b/src/Core/System/Test/SalesChannel/Context/CartRestorerTest.php
@@ -10,17 +10,14 @@ use Shopware\Core\Checkout\Cart\CartRuleLoader;
 use Shopware\Core\Checkout\Cart\Event\CartMergedEvent;
 use Shopware\Core\Checkout\Cart\LineItem\LineItem;
 use Shopware\Core\Checkout\Cart\LineItem\LineItemCollection;
 use Shopware\Core\Checkout\Cart\SalesChannel\CartService;
 use Shopware\Core\Checkout\Customer\CustomerEntity;
-use Shopware\Core\Checkout\Payment\Cart\PaymentHandler\PrePayment;
 use Shopware\Core\Content\Product\Aggregate\ProductVisibility\ProductVisibilityDefinition;
 use Shopware\Core\Defaults;
 use Shopware\Core\Framework\Context;
-use Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface;
 use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
-use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
 use Shopware\Core\Framework\Feature;
 use Shopware\Core\Framework\Test\TestCaseBase\IntegrationTestBehaviour;
 use Shopware\Core\Framework\Util\Random;
 use Shopware\Core\Framework\Uuid\Uuid;
 use Shopware\Core\System\SalesChannel\Context\AbstractSalesChannelContextFactory;
@@ -31,10 +28,11 @@ use Shopware\Core\System\SalesChannel\Context\SalesChannelContextService;
 use Shopware\Core\System\SalesChannel\Event\SalesChannelContextRestoredEvent;
 use Shopware\Core\System\SalesChannel\SalesChannelContext;
 use Shopware\Core\Test\TestDefaults;
 use Symfony\Component\EventDispatcher\EventDispatcher;
 use Symfony\Contracts\EventDispatcher\Event;
+use function json_encode;
 
 class CartRestorerTest extends TestCase
 {
     use IntegrationTestBehaviour;
 
@@ -323,37 +321,29 @@ class CartRestorerTest extends TestCase
         static::assertEquals(1, $p1->getQuantity());
         static::assertNotEmpty($p2 = $restoreCart->getLineItems()->get($productId2));
         static::assertEquals(5, $p2->getQuantity());
     }
 
-    private function getStateId(string $state, string $machine)
+    public function testPermissionsAreIgnoredOnRestoer(): void
     {
-        return $this->getContainer()->get(Connection::class)
-            ->fetchColumn('
-                SELECT LOWER(HEX(state_machine_state.id))
-                FROM state_machine_state
-                    INNER JOIN  state_machine
-                    ON state_machine.id = state_machine_state.state_machine_id
-                    AND state_machine.technical_name = :machine
-                WHERE state_machine_state.technical_name = :state
-            ', [
-                'state' => $state,
-                'machine' => $machine,
-            ]);
-    }
+        $currentContextToken = Random::getAlphanumericString(32);
 
-    private function getPrePaymentMethodId(): string
-    {
-        /** @var EntityRepositoryInterface $repository */
-        $repository = $this->getContainer()->get('payment_method.repository');
+        $currentContext = $this->createSalesChannelContext($currentContextToken, []);
 
-        $criteria = (new Criteria())
-            ->setLimit(1)
-            ->addFilter(new EqualsFilter('active', true))
-            ->addFilter(new EqualsFilter('handlerIdentifier', PrePayment::class));
+        $con = $this->getContainer()->get(Connection::class);
+
+        $con->insert('sales_channel_api_context', [
+            'token' => Random::getAlphanumericString(32),
+            'payload' => json_encode(['expired' => false, 'customerId' => $this->customerId, 'permissions' => ['foo']], \JSON_THROW_ON_ERROR),
+            'sales_channel_id' => Uuid::fromHexToBytes($currentContext->getSalesChannelId()),
+            'customer_id' => Uuid::fromHexToBytes($this->customerId),
+            'updated_at' => (new \DateTime())->format(Defaults::STORAGE_DATE_TIME_FORMAT),
+        ]);
+
+        $restoreContext = $this->cartRestorer->restore($this->customerId, $currentContext);
 
-        return $repository->searchIds($criteria, Context::createDefaultContext())->getIds()[0];
+        static::assertSame([], $restoreContext->getPermissions());
     }
 
     private function createProduct(Context $context): string
     {
         $productId = Uuid::randomHex();

Vulnerable code:         if (empty($customerPayload) || !($customerPayload['expired'] ?? false) && $customerPayload['token'] === $currentContext->getToken()) {
    public function save(string $token, array $parameters, string $salesChannelId, ?string $customerId = null): void
        $parameters = array_replace_recursive($existing, $parameters);
use Shopware\Core\Checkout\Payment\Cart\PaymentHandler\PrePayment;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
    private function getStateId(string $state, string $machine)
        return $this->getContainer()->get(Connection::class)
            ->fetchColumn('
                SELECT LOWER(HEX(state_machine_state.id))
                FROM state_machine_state
                    INNER JOIN  state_machine
                    ON state_machine.id = state_machine_state.state_machine_id
                    AND state_machine.technical_name = :machine
                WHERE state_machine_state.technical_name = :state
            ', [
                'state' => $state,
                'machine' => $machine,
            ]);
    }
    private function getPrePaymentMethodId(): string
    {
        /** @var EntityRepositoryInterface $repository */
        $repository = $this->getContainer()->get('payment_method.repository');
        $criteria = (new Criteria())
            ->setLimit(1)
            ->addFilter(new EqualsFilter('active', true))
            ->addFilter(new EqualsFilter('handlerIdentifier', PrePayment::class));
        return $repository->searchIds($criteria, Context::createDefaultContext())->getIds()[0];
Fixed code:         $response->headers->set('content-type', 'application/json');
        if (empty($customerPayload) || !empty($customerPayload['permissions']) || !($customerPayload['expired'] ?? false) && $customerPayload['token'] === $currentContext->getToken()) {
                'permissions' => [],
    public function save(string $token, array $newParameters, string $salesChannelId, ?string $customerId = null): void
        $parameters = array_replace_recursive($existing, $newParameters);
        if (isset($newParameters['permissions']) && $newParameters['permissions'] === []) {
            $parameters['permissions'] = [];
        }
use function json_encode;
    public function testPermissionsAreIgnoredOnRestoer(): void
        $currentContextToken = Random::getAlphanumericString(32);
        $currentContext = $this->createSalesChannelContext($currentContextToken, []);
        $con = $this->getContainer()->get(Connection::class);

        $con->insert('sales_channel_api_context', [
            'token' => Random::getAlphanumericString(32),
            'payload' => json_encode(['expired' => false, 'customerId' => $this->customerId, 'permissions' => ['foo']], \JSON_THROW_ON_ERROR),
            'sales_channel_id' => Uuid::fromHexToBytes($currentContext->getSalesChannelId()),
            'customer_id' => Uuid::fromHexToBytes($this->customerId),
            'updated_at' => (new \DateTime())->format(Defaults::STORAGE_DATE_TIME_FORMAT),
        ]);

        $restoreContext = $this->cartRestorer->restore($this->customerId, $currentContext);
        static::assertSame([], $restoreContext->getPermissions());