CVE-2022-24871: Server-Side Request Forgery (SSRF) (CWE-918)
value: Shopware is an open commerce platform based on Symfony Framework and Vue. In affected versions an attacker can abuse the Admin SDK functionality on the server to read or update internal resources. 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 web server receives a URL or similar request from an upstream component and retrieves the contents of this URL, but it does not sufficiently ensure that the request is being sent to the expected destination.
CVSS v3 base score: 5.5
Published: 2022-04-20T19: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());