CVE-2022-24748: Incorrect Authorization (CWE-863)
value: Shopware is an open commerce platform based on the Symfony php Framework and the Vue javascript framework. In versions prior to 6.4.8.2 it is possible to modify customers and to create orders without App Permission. This issue is a result of improper api route checking. Users are advised to upgrade to version 6.4.8.2. There are no known workarounds.
CWE description: The product performs an authorization check when an actor attempts to access a resource or perform an action, but it does not correctly perform the check. This allows attackers to bypass intended access restrictions.
CVSS v3 base score: 7.5
Published: 2022-03-09T23:15Z
Repository: https://github.com/shopware/core
Language: PHP
Files changed: Framework/Api/Controller/SalesChannelProxyController.php, Framework/Test/Api/Controller/SalesChannelProxyControllerTest.php, Migration/Migration1646397836UpdateRolePrivilegesOfOrderCreatorUpdateRolePrivilegesOfOrderCreator.php, Migration/Test/Migration1646397836UpdateRolePrivilegesOfOrderCreatorTest.php, Migration/V6_4/Migration1646397836UpdateRolePrivilegesOfOrderCreator.php
Commit date: 2022-03-04 14:34:04 +0100
Commit message: NEXT-20305 - Secure proxy route to switch customer with ACL

Diff: diff --git a/Framework/Api/Controller/SalesChannelProxyController.php b/Framework/Api/Controller/SalesChannelProxyController.php
index f47b815ad..0285cdc59 100644
--- a/Framework/Api/Controller/SalesChannelProxyController.php
+++ b/Framework/Api/Controller/SalesChannelProxyController.php
@@ -18,10 +18,11 @@ use Shopware\Core\Framework\Context;
 use Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface;
 use Shopware\Core\Framework\DataAbstractionLayer\Exception\InconsistentCriteriaIdsException;
 use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
 use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
 use Shopware\Core\Framework\DataAbstractionLayer\Validation\EntityExists;
+use Shopware\Core\Framework\Routing\Annotation\Acl;
 use Shopware\Core\Framework\Routing\Annotation\RouteScope;
 use Shopware\Core\Framework\Routing\Annotation\Since;
 use Shopware\Core\Framework\Routing\Exception\MissingRequestParameterException;
 use Shopware\Core\Framework\Routing\SalesChannelRequestContextResolver;
 use Shopware\Core\Framework\Util\Random;
@@ -165,10 +166,11 @@ class SalesChannelProxyController extends AbstractController
     }
 
     /**
      * @Since("6.2.0.0")
      * @Route("/api/_proxy/switch-customer", name="api.proxy.switch-customer", methods={"PATCH"})
+     * @Acl({"api_proxy_switch-customer"})
      *
      * @throws InconsistentCriteriaIdsException
      * @throws InvalidSalesChannelIdException
      * @throws MissingRequestParameterException
      */
diff --git a/Framework/Test/Api/Controller/SalesChannelProxyControllerTest.php b/Framework/Test/Api/Controller/SalesChannelProxyControllerTest.php
index fc5df2e89..35e46b39b 100644
--- a/Framework/Test/Api/Controller/SalesChannelProxyControllerTest.php
+++ b/Framework/Test/Api/Controller/SalesChannelProxyControllerTest.php
@@ -1060,10 +1060,11 @@ class SalesChannelProxyControllerTest extends TestCase
             static::assertCount(2, $cart['lineItems']);
             static::assertSame('product', $cart['lineItems'][0]['type']);
             static::assertSame('credit', $cart['lineItems'][1]['type']);
 
             $orderPrivileges = [
+                'api_proxy_switch-customer',
                 'order:create',
                 'order_customer:create',
                 'order_address:create',
                 'order_delivery:create',
                 'order_line_item:create',
@@ -1073,11 +1074,11 @@ class SalesChannelProxyControllerTest extends TestCase
                 'customer:update',
             ];
             foreach ([true, false] as $testOrderOnly) {
                 TestUser::createNewTestUser(
                     $browser->getContainer()->get(Connection::class),
-                    $testOrderOnly ? $orderPrivileges : [CreditOrderLineItemListener::ACL_ORDER_CREATE_DISCOUNT_PRIVILEGE],
+                    $testOrderOnly ? $orderPrivileges : ['api_proxy_switch-customer', CreditOrderLineItemListener::ACL_ORDER_CREATE_DISCOUNT_PRIVILEGE],
                 )->authorizeBrowser($browser);
                 $browser->request('POST', $this->getCreateOrderApiUrl($salesChannelContext->getSalesChannel()->getId()));
 
                 $response = $browser->getResponse()->getContent();
                 $response = json_decode($response, true);
diff --git a/Migration/Migration1646397836UpdateRolePrivilegesOfOrderCreatorUpdateRolePrivilegesOfOrderCreator.php b/Migration/Migration1646397836UpdateRolePrivilegesOfOrderCreatorUpdateRolePrivilegesOfOrderCreator.php
new file mode 100644
index 000000000..5f86ed45a
--- /dev/null
+++ b/Migration/Migration1646397836UpdateRolePrivilegesOfOrderCreatorUpdateRolePrivilegesOfOrderCreator.php
@@ -0,0 +1,10 @@
+<?php declare(strict_types=1);
+
+namespace Shopware\Core\Migration;
+
+/**
+ * @deprecated tag:v6.5.0 - Will be deleted. Migrations are now namespaced by major version
+ */
+class Migration1646397836UpdateRolePrivilegesOfOrderCreatorUpdateRolePrivilegesOfOrderCreator extends \Shopware\Core\Migration\V6_4\Migration1646397836UpdateRolePrivilegesOfOrderCreator
+{
+}
diff --git a/Migration/Test/Migration1646397836UpdateRolePrivilegesOfOrderCreatorTest.php b/Migration/Test/Migration1646397836UpdateRolePrivilegesOfOrderCreatorTest.php
new file mode 100644
index 000000000..ef338a62e
--- /dev/null
+++ b/Migration/Test/Migration1646397836UpdateRolePrivilegesOfOrderCreatorTest.php
@@ -0,0 +1,64 @@
+<?php declare(strict_types=1);
+
+namespace Shopware\Core\Migration\Test;
+
+use Doctrine\DBAL\Connection;
+use PHPUnit\Framework\TestCase;
+use Shopware\Core\Framework\Api\Acl\Role\AclRoleEntity;
+use Shopware\Core\Framework\Context;
+use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
+use Shopware\Core\Framework\Test\TestCaseBase\IntegrationTestBehaviour;
+use Shopware\Core\Framework\Uuid\Uuid;
+use Shopware\Core\Migration\V6_4\Migration1646397836UpdateRolePrivilegesOfOrderCreator;
+
+class Migration1646397836UpdateRolePrivilegesOfOrderCreatorTest extends TestCase
+{
+    use IntegrationTestBehaviour;
+
+    public function testNewPermissionsAreAdded(): void
+    {
+        $repo = $this->getContainer()->get('acl_role.repository');
+        $connection = $this->getContainer()->get(Connection::class);
+
+        $id = Uuid::randomHex();
+        $context = Context::createDefaultContext();
+        $repo->create([[
+            'id' => $id,
+            'name' => 'test',
+            'privileges' => ['order.creator'],
+        ]], $context);
+
+        $migration = new Migration1646397836UpdateRolePrivilegesOfOrderCreator();
+        $migration->update($connection);
+
+        /** @var AclRoleEntity $role */
+        $role = $repo->search(new Criteria([$id]), $context)->first();
+        static::assertNotNull($role);
+
+        static::assertContains('api_proxy_switch-customer', $role->getPrivileges());
+    }
+
+    public function testUnrelatedRolesAreNotUpdated(): void
+    {
+        $repo = $this->getContainer()->get('acl_role.repository');
+        $connection = $this->getContainer()->get(Connection::class);
+
+        $id = Uuid::randomHex();
+        $context = Context::createDefaultContext();
+        $privileges = ['order:create', 'order:read', 'order:update', 'order:delete'];
+        $repo->create([[
+            'id' => $id,
+            'name' => 'test',
+            'privileges' => $privileges,
+        ]], $context);
+
+        $before = $connection->fetchAssociative('SELECT * FROM `acl_role` WHERE id = :id', ['id' => Uuid::fromHexToBytes($id)]);
+
+        $migration = new Migration1646397836UpdateRolePrivilegesOfOrderCreator();
+        $migration->update($connection);
+
+        $after = $connection->fetchAssociative('SELECT * FROM `acl_role` WHERE id = :id', ['id' => Uuid::fromHexToBytes($id)]);
+
+        static::assertSame($before, $after);
+    }
+}
diff --git a/Migration/V6_4/Migration1646397836UpdateRolePrivilegesOfOrderCreator.php b/Migration/V6_4/Migration1646397836UpdateRolePrivilegesOfOrderCreator.php
new file mode 100644
index 000000000..22151619b
--- /dev/null
+++ b/Migration/V6_4/Migration1646397836UpdateRolePrivilegesOfOrderCreator.php
@@ -0,0 +1,56 @@
+<?php declare(strict_types=1);
+
+namespace Shopware\Core\Migration\V6_4;
+
+use Doctrine\DBAL\Connection;
+use Shopware\Core\Defaults;
+use Shopware\Core\Framework\Migration\MigrationStep;
+
+class Migration1646397836UpdateRolePrivilegesOfOrderCreator extends MigrationStep
+{
+    public const NEW_PRIVILEGES = [
+        'order.creator' => [
+            'api_proxy_switch-customer',
+        ],
+    ];
+
+    public function getCreationTimestamp(): int
+    {
+        return 1646397836;
+    }
+
+    public function update(Connection $connection): void
+    {
+        $roles = $connection->fetchAllAssociative('SELECT * from `acl_role`');
+
+        foreach ($roles as $role) {
+            $currentPrivileges = \json_decode($role['privileges'], true, 512, \JSON_THROW_ON_ERROR);
+            $newPrivileges = array_values($this->fixRolePrivileges($currentPrivileges));
+
+            if ($currentPrivileges === $newPrivileges) {
+                continue;
+            }
+
+            $role['privileges'] = json_encode($newPrivileges);
+            $role['updated_at'] = (new \DateTimeImmutable())->format(Defaults::STORAGE_DATE_FORMAT);
+
+            $connection->update('acl_role', $role, ['id' => $role['id']]);
+        }
+    }
+
+    public function updateDestructive(Connection $connection): void
+    {
+        // implement update destructive
+    }
+
+    private function fixRolePrivileges(array $rolePrivileges): array
+    {
+        foreach (self::NEW_PRIVILEGES as $key => $new) {
+            if (\in_array($key, $rolePrivileges, true)) {
+                $rolePrivileges = array_merge($rolePrivileges, $new);
+            }
+        }
+
+        return array_unique($rolePrivileges);
+    }
+}

Vulnerable code:                     $testOrderOnly ? $orderPrivileges : [CreditOrderLineItemListener::ACL_ORDER_CREATE_DISCOUNT_PRIVILEGE],
Fixed code: use Shopware\Core\Framework\Routing\Annotation\Acl;
     * @Acl({"api_proxy_switch-customer"})
                'api_proxy_switch-customer',
                    $testOrderOnly ? $orderPrivileges : ['api_proxy_switch-customer', CreditOrderLineItemListener::ACL_ORDER_CREATE_DISCOUNT_PRIVILEGE],
<?php declare(strict_types=1);

namespace Shopware\Core\Migration;

/**
 * @deprecated tag:v6.5.0 - Will be deleted. Migrations are now namespaced by major version
 */
class Migration1646397836UpdateRolePrivilegesOfOrderCreatorUpdateRolePrivilegesOfOrderCreator extends \Shopware\Core\Migration\V6_4\Migration1646397836UpdateRolePrivilegesOfOrderCreator
{
}
<?php declare(strict_types=1);

namespace Shopware\Core\Migration\Test;

use Doctrine\DBAL\Connection;
use PHPUnit\Framework\TestCase;
use Shopware\Core\Framework\Api\Acl\Role\AclRoleEntity;
use Shopware\Core\Framework\Context;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\Test\TestCaseBase\IntegrationTestBehaviour;
use Shopware\Core\Framework\Uuid\Uuid;
use Shopware\Core\Migration\V6_4\Migration1646397836UpdateRolePrivilegesOfOrderCreator;

class Migration1646397836UpdateRolePrivilegesOfOrderCreatorTest extends TestCase
{
    use IntegrationTestBehaviour;

    public function testNewPermissionsAreAdded(): void
    {
        $repo = $this->getContainer()->get('acl_role.repository');
        $connection = $this->getContainer()->get(Connection::class);

        $id = Uuid::randomHex();
        $context = Context::createDefaultContext();
        $repo->create([[
            'id' => $id,
            'name' => 'test',
            'privileges' => ['order.creator'],
        ]], $context);

        $migration = new Migration1646397836UpdateRolePrivilegesOfOrderCreator();
        $migration->update($connection);

        /** @var AclRoleEntity $role */
        $role = $repo->search(new Criteria([$id]), $context)->first();
        static::assertNotNull($role);

        static::assertContains('api_proxy_switch-customer', $role->getPrivileges());
    }

    public function testUnrelatedRolesAreNotUpdated(): void
    {
        $repo = $this->getContainer()->get('acl_role.repository');
        $connection = $this->getContainer()->get(Connection::class);

        $id = Uuid::randomHex();
        $context = Context::createDefaultContext();
        $privileges = ['order:create', 'order:read', 'order:update', 'order:delete'];
        $repo->create([[
            'id' => $id,
            'name' => 'test',
            'privileges' => $privileges,
        ]], $context);

        $before = $connection->fetchAssociative('SELECT * FROM `acl_role` WHERE id = :id', ['id' => Uuid::fromHexToBytes($id)]);

        $migration = new Migration1646397836UpdateRolePrivilegesOfOrderCreator();
        $migration->update($connection);

        $after = $connection->fetchAssociative('SELECT * FROM `acl_role` WHERE id = :id', ['id' => Uuid::fromHexToBytes($id)]);

        static::assertSame($before, $after);
    }
}
<?php declare(strict_types=1);

namespace Shopware\Core\Migration\V6_4;

use Doctrine\DBAL\Connection;
use Shopware\Core\Defaults;
use Shopware\Core\Framework\Migration\MigrationStep;

class Migration1646397836UpdateRolePrivilegesOfOrderCreator extends MigrationStep
{
    public const NEW_PRIVILEGES = [
        'order.creator' => [
            'api_proxy_switch-customer',
        ],
    ];

    public function getCreationTimestamp(): int
    {
        return 1646397836;
    }

    public function update(Connection $connection): void
    {
        $roles = $connection->fetchAllAssociative('SELECT * from `acl_role`');

        foreach ($roles as $role) {
            $currentPrivileges = \json_decode($role['privileges'], true, 512, \JSON_THROW_ON_ERROR);
            $newPrivileges = array_values($this->fixRolePrivileges($currentPrivileges));

            if ($currentPrivileges === $newPrivileges) {
                continue;
            }

            $role['privileges'] = json_encode($newPrivileges);
            $role['updated_at'] = (new \DateTimeImmutable())->format(Defaults::STORAGE_DATE_FORMAT);

            $connection->update('acl_role', $role, ['id' => $role['id']]);
        }
    }

    public function updateDestructive(Connection $connection): void
    {
        // implement update destructive
    }

    private function fixRolePrivileges(array $rolePrivileges): array
    {
        foreach (self::NEW_PRIVILEGES as $key => $new) {
            if (\in_array($key, $rolePrivileges, true)) {
                $rolePrivileges = array_merge($rolePrivileges, $new);
            }
        }

        return array_unique($rolePrivileges);
    }
}