CVE-2021-32716 (PHP, published 2021-06-24T21:15Z)
Description: value: Shopware is an open source eCommerce platform. In versions prior to 6.4.1.1 the admin api has exposed some internal hidden fields when an association has been loaded with a to many reference. Users are recommend to update to version 6.4.1.1. You can get the update to 6.4.1.1 regularly via the Auto-Updater or directly via the download overview. For older versions of 6.1, 6.2, and 6.3, corresponding security measures are also available via a plugin.
Severity: MEDIUM (CVSS v3 base score: 4.9)
Weakness: CWE-863 \u2014 Incorrect Authorization
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.
Repository: https://github.com/shopware/platform
Files changed:
.gitlab-ci.yml
dangerfile.js
src/Core/Checkout/DependencyInjection/order.xml
src/Core/Checkout/Order/SalesChannel/CancelOrderRoute.php
src/Core/Checkout/Test/Order/SalesChannel/CancelOrderRouteTest.php
src/Core/Framework/Test/TestCaseBase/BasicTestDataBehaviour.php
Commit message: NEXT-15183 - Add Dangerfile


Vulnerable code:
    /**
     * @var OrderService
     */
    private $orderService;
    public function __construct(OrderService $orderService)
            $request->get('orderId'),

Fixed code:
Danger:
    stage: unit
    image: node:14
    before_script: []
    script:
        - npm install danger gitlab
        - node node_modules/.bin/danger ci --base trunk

const GitlabClient = require('gitlab');

const api = new GitlabClient.Gitlab({
    token: process.env['DANGER_GITLAB_API_TOKEN'],
    host: process.env['DANGER_GITLAB_HOST']
});

const addLabel = (labels) => {
    const currentLabels = danger.gitlab.mr.labels;

    for (const label of labels) {
        currentLabels.push(label);
    }

    api.MergeRequests.edit(1, process.env['CI_MERGE_REQUEST_IID'], {
        'labels': currentLabels.join(',')
    })
};

const removeLabel = (labels) => {
    const currentLabels = danger.gitlab.mr.labels;

    for (const label of labels) {
        const index = currentLabels.indexOf(label);
        if (index > -1) {
            currentLabels.splice(index, 1);
        }

        api.MergeRequests.edit(1, process.env['CI_MERGE_REQUEST_IID'], {
            'labels': currentLabels.join(',')
        })
    }
};

const hasStoreApiRouteChanges = () => {
    for (let file of danger.git.modified_files) {
        if (file.includes('SalesChannel') && file.includes('Route.php') && !file.includes('/Test/')) {
            return true;
        }
    }

    for (let file of danger.git.created_files) {
        if (file.includes('SalesChannel') && file.includes('Route.php') && !file.includes('/Test/')) {
            return true;
        }
    }

    return false;
}

if (hasStoreApiRouteChanges()) {
    warn('Store-API Route has been modified. @Reviewers please review carefully!')
    addLabel(['Security-Audit Required']);
}
            <argument type="service" id="order.repository"/>
use Shopware\Core\Checkout\Cart\Exception\CustomerNotLoggedInException;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface;
use Shopware\Core\Framework\DataAbstractionLayer\Exception\EntityNotFoundException;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
use Shopware\Core\Framework\Routing\Exception\InvalidRequestParameterException;
    private OrderService $orderService;

    private EntityRepositoryInterface $orderRepository;
    public function __construct(OrderService $orderService, EntityRepositoryInterface $orderRepository)
        $this->orderRepository = $orderRepository;
        $orderId = $request->get('orderId', null);

        if ($orderId === null) {
            throw new InvalidRequestParameterException('orderId');
        }

        $this->verify($orderId, $context);

            $orderId,

    private function verify(string $orderId, SalesChannelContext $context): void
    {
        if ($context->getCustomer() === null) {
            throw new CustomerNotLoggedInException();
        }

        $criteria = new Criteria([$orderId]);
        $criteria->addFilter(new EqualsFilter('orderCustomer.customerId', $context->getCustomer()->getId()));

        if ($this->orderRepository->searchIds($criteria, $context->getContext())->firstId() === null) {
            throw new EntityNotFoundException('order', $orderId);
        }
    }
<?php declare(strict_types=1);

namespace Shopware\Core\Checkout\Test\Order\SalesChannel;

use PHPUnit\Framework\TestCase;
use Shopware\Core\Checkout\Cart\LineItem\LineItem;
use Shopware\Core\Checkout\Cart\Price\Struct\CalculatedPrice;
use Shopware\Core\Checkout\Cart\Price\Struct\CartPrice;
use Shopware\Core\Checkout\Cart\Price\Struct\QuantityPriceDefinition;
use Shopware\Core\Checkout\Cart\Tax\Struct\CalculatedTaxCollection;
use Shopware\Core\Checkout\Cart\Tax\Struct\TaxRuleCollection;
use Shopware\Core\Checkout\Order\OrderEntity;
use Shopware\Core\Checkout\Test\Customer\SalesChannel\CustomerTestTrait;
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\Test\IdsCollection;
use Shopware\Core\Framework\Test\TestCaseBase\IntegrationTestBehaviour;
use Shopware\Core\Framework\Test\TestDataCollection;
use Shopware\Core\Framework\Uuid\Uuid;
use Symfony\Bundle\FrameworkBundle\KernelBrowser;
use Symfony\Component\HttpFoundation\Response;

class CancelOrderRouteTest extends TestCase
{
    use IntegrationTestBehaviour;
    use CustomerTestTrait;

    private KernelBrowser $browser;

    private EntityRepositoryInterface $customerRepository;

    private IdsCollection $ids;

    protected function setUp(): void
    {
        parent::setUp();

        $this->customerRepository = $this->getContainer()->get('customer.repository');
        $this->ids = new TestDataCollection();

        $this->browser = $this->createCustomSalesChannelBrowser([
            'id' => $this->ids->create('sales-channel'),
        ]);

        $this->assignSalesChannelContext($this->browser);

        $email = Uuid::randomHex() . '@example.com';
        $customerId = $this->createCustomer('shopware', $email);

        $this->ids->set('order-1', $this->createOrder($this->ids, $customerId));
        $this->ids->set('order-2', $this->createOrder($this->ids, $this->createCustomer('test', 'test-other@test.de')));

        $this->browser
            ->request(
                'POST',
                '/store-api/account/login',
                [
                    'email' => $email,
                    'password' => 'shopware',
                ]
            );

        $response = json_decode($this->browser->getResponse()->getContent(), true);

        $this->browser->setServerParameter('HTTP_SW_CONTEXT_TOKEN', $response['contextToken']);
    }

    public function testCancelMyOwnOrder(): void
    {
        $this->browser
            ->request(
                'POST',
                '/store-api/order/state/cancel',
                [
                    'orderId' => $this->ids->get('order-1'),
                ]
            );

        $response = json_decode($this->browser->getResponse()->getContent(), true);

        static::assertSame(Response::HTTP_OK, $this->browser->getResponse()->getStatusCode());
        static::assertSame('cancelled', $response['technicalName']);

        $criteria = new Criteria([$this->ids->get('order-1')]);
        $criteria->addAssociation('stateMachineState');

        /** @var OrderEntity $order */
        $order = $this->getContainer()->get('order.repository')->search($criteria, Context::createDefaultContext())->first();

        static::assertSame('cancelled', $order->getStateMachineState()->getTechnicalName());
    }

    public function testCancelRandomOrder(): void
    {
        $this->browser
            ->request(
                'POST',
                '/store-api/order/state/cancel',
                [
                    'orderId' => Uuid::randomHex(),
                ]
            );

        $response = json_decode($this->browser->getResponse()->getContent(), true);

        static::assertSame(Response::HTTP_NOT_FOUND, $this->browser->getResponse()->getStatusCode());
        static::assertSame('FRAMEWORK__ENTITY_NOT_FOUND', $response['errors'][0]['code']);
    }

    public function testCancelOtherUsersOrder(): void
    {
        $this->browser
            ->request(
                'POST',
                '/store-api/order/state/cancel',
                [
                    'orderId' => $this->ids->get('order-2'),
                ]
            );

        $response = json_decode($this->browser->getResponse()->getContent(), true);

        static::assertSame(Response::HTTP_NOT_FOUND, $this->browser->getResponse()->getStatusCode());
        static::assertSame('FRAMEWORK__ENTITY_NOT_FOUND', $response['errors'][0]['code']);
    }

    public function testCancelWithoutLogin(): void
    {
        $this->browser = $this->createCustomSalesChannelBrowser([
            'id' => Uuid::randomHex(),
        ]);

        $this->browser
            ->request(
                'POST',
                '/store-api/order/state/cancel',
                [
                    'orderId' => $this->ids->get('order-2'),
                ]
            );

        $response = json_decode($this->browser->getResponse()->getContent(), true);

        static::assertSame(Response::HTTP_FORBIDDEN, $this->browser->getResponse()->getStatusCode());
        static::assertSame('CHECKOUT__CUSTOMER_NOT_LOGGED_IN', $response['errors'][0]['code']);
    }

    private function createOrder(IdsCollection $ids, string $customerId): string
    {
        $id = Uuid::randomHex();

        $this->getContainer()->get('order.repository')->create(
            [[
                'id' => $id,
                'orderDateTime' => (new \DateTimeImmutable())->format(Defaults::STORAGE_DATE_TIME_FORMAT),
                'price' => new CartPrice(10, 10, 10, new CalculatedTaxCollection(), new TaxRuleCollection(), CartPrice::TAX_STATE_NET),
                'shippingCosts' => new CalculatedPrice(10, 10, new CalculatedTaxCollection(), new TaxRuleCollection()),
                'orderCustomer' => [
                    'customerId' => $customerId,
                    'email' => 'test@example.com',
                    'salutationId' => $this->getValidSalutationId(),
                    'firstName' => 'Max',
                    'lastName' => 'Mustermann',
                ],
                'stateId' => $this->getStateMachineState(),
                'paymentMethodId' => $this->getValidPaymentMethodId(),
                'currencyId' => Defaults::CURRENCY,
                'currencyFactor' => 1.0,
                'salesChannelId' => Defaults::SALES_CHANNEL,
                'billingAddressId' => $billingAddressId = Uuid::randomHex(),
                'addresses' => [
                    [
                        'id' => $billingAddressId,
                        'salutationId' => $this->getValidSalutationId(),
                        'firstName' => 'Max',
                        'lastName' => 'Mustermann',
                        'street' => 'Ebbinghoff 10',
                        'zipcode' => '48624',
                        'city' => 'Schöppingen',
                        'countryId' => $this->getValidCountryId(),
                    ],
                ],
                'lineItems' => [
                    [
                        'id' => $ids->get('VoucherA'),
                        'type' => LineItem::PROMOTION_LINE_ITEM_TYPE,
                        'code' => $ids->get('VoucherA'),
                        'identifier' => $ids->get('VoucherA'),
                        'quantity' => 1,
                        'payload' => ['promotionId' => $ids->get('voucherA')],
                        'label' => 'label',
                        'price' => new CalculatedPrice(200, 200, new CalculatedTaxCollection(), new TaxRuleCollection()),
                        'priceDefinition' => new QuantityPriceDefinition(200, new TaxRuleCollection(), 2),
                    ],
                    [
                        'id' => $ids->get('VoucherC'),
                        'type' => LineItem::PROMOTION_LINE_ITEM_TYPE,
                        'code' => $ids->get('VoucherC'),
                        'identifier' => $ids->get('VoucherC'),
                        'payload' => ['promotionId' => $ids->get('voucherB')],
                        'quantity' => 1,
                        'label' => 'label',
                        'price' => new CalculatedPrice(200, 200, new CalculatedTaxCollection(), new TaxRuleCollection()),
                        'priceDefinition' => new QuantityPriceDefinition(200, new TaxRuleCollection(), 2),
                    ],
                    [
                        'id' => $ids->get('VoucherB'),
                        'type' => LineItem::PROMOTION_LINE_ITEM_TYPE,
                        'code' => $ids->get('VoucherB'),
                        'identifier' => $ids->get('VoucherB'),
                        'payload' => ['promotionId' => $ids->get('voucherB')],
                        'quantity' => 1,
                        'label' => 'label',
                        'price' => new CalculatedPrice(200, 200, new CalculatedTaxCollection(), new TaxRuleCollection()),
                        'priceDefinition' => new QuantityPriceDefinition(200, new TaxRuleCollection(), 2),
                    ],
                ],
                'deliveries' => [],
                'context' => '{}',
                'payload' => '{}',
            ]],
            Context::createDefaultContext()
        );

        return $id;
    }
}
use Shopware\Core\Checkout\Order\OrderStates;

    protected function getStateMachineState(string $stateMachine = OrderStates::STATE_MACHINE, string $state = OrderStates::STATE_OPEN): string
    {
        /** @var EntityRepositoryInterface $repository */
        $repository = $this->getContainer()->get('state_machine_state.repository');

        $criteria = new Criteria();
        $criteria
            ->setLimit(1)
            ->addFilter(new EqualsFilter('technicalName', $state))
            ->addFilter(new EqualsFilter('stateMachine.technicalName', $stateMachine));

        return $repository->searchIds($criteria, Context::createDefaultContext())->getIds()[0];
    }