CVE-2022-24746: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (CWE-79)
value: Shopware is an open commerce platform based on the Symfony php Framework and the Vue javascript framework. In affected versions it is possible to inject code via the voucher code form. This issue has been patched in version 6.4.8.1. There are no known workarounds for this issue.
CWE description: The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.
CVSS v3 base score: 6.1
Published: 2022-03-09T23:15Z
Repository: https://github.com/shopware/platform
Language: PHP
Files changed: changelog/_unreleased/2022-02-03-add-filtering-to-product-and-promotion-codes.md, src/Core/Checkout/Cart/LineItemFactoryHandler/PromotionLineItemFactory.php, src/Core/Checkout/DependencyInjection/promotion.xml, src/Core/Checkout/Promotion/Cart/PromotionCollector.php, src/Storefront/Controller/CartLineItemController.php, src/Storefront/DependencyInjection/controller.xml, src/Storefront/Test/Controller/CartLineItemControllerTest.php
Commit date: 2022-02-04 13:20:46 +0100
Commit message: NEXT-19276 - Add filtering to promotion and product codes

Diff: diff --git a/changelog/_unreleased/2022-02-03-add-filtering-to-product-and-promotion-codes.md b/changelog/_unreleased/2022-02-03-add-filtering-to-product-and-promotion-codes.md
new file mode 100644
index 00000000000..0832563c084
--- /dev/null
+++ b/changelog/_unreleased/2022-02-03-add-filtering-to-product-and-promotion-codes.md
@@ -0,0 +1,8 @@
+---
+title: Add filtering to product and promotion codes
+issue: NEXT-19276
+author: Max Stegmeyer
+author_email: m.stegmeyer@shopware.com
+---
+# Storefront
+* Changed `Shopware\Storefront\Controller\CartLineItemController` to sanitize from promotion codes and product number error messages
diff --git a/src/Core/Checkout/Cart/LineItemFactoryHandler/PromotionLineItemFactory.php b/src/Core/Checkout/Cart/LineItemFactoryHandler/PromotionLineItemFactory.php
index f35a275bc88..09c0513c813 100644
--- a/src/Core/Checkout/Cart/LineItemFactoryHandler/PromotionLineItemFactory.php
+++ b/src/Core/Checkout/Cart/LineItemFactoryHandler/PromotionLineItemFactory.php
@@ -13,17 +13,19 @@ class PromotionLineItemFactory implements LineItemFactoryInterface
         return $type === LineItem::PROMOTION_LINE_ITEM_TYPE;
     }
 
     public function create(array $data, SalesChannelContext $context): LineItem
     {
-        $uniqueKey = 'promotion-' . $data['referencedId'];
+        $code = $data['referencedId'];
+
+        $uniqueKey = 'promotion-' . $code;
         $item = new LineItem($uniqueKey, LineItem::PROMOTION_LINE_ITEM_TYPE);
         $item->setLabel($uniqueKey);
         $item->setGood(false);
 
         // this is used to pass on the code for later usage
-        $item->setReferencedId($data['referencedId']);
+        $item->setReferencedId($code);
 
         // this is important to avoid any side effects when calculating the cart
         // a percentage of 0,00 will just do nothing
         $item->setPriceDefinition(new PercentagePriceDefinition(0));
 
diff --git a/src/Core/Checkout/DependencyInjection/promotion.xml b/src/Core/Checkout/DependencyInjection/promotion.xml
index 2c68860a482..49893a93833 100644
--- a/src/Core/Checkout/DependencyInjection/promotion.xml
+++ b/src/Core/Checkout/DependencyInjection/promotion.xml
@@ -68,10 +68,11 @@
         <service id="Shopware\Core\Checkout\Promotion\Cart\PromotionItemBuilder"/>
 
         <service id="Shopware\Core\Checkout\Promotion\Cart\PromotionCollector">
             <argument type="service" id="Shopware\Core\Checkout\Promotion\Gateway\PromotionGateway"/>
             <argument type="service" id="Shopware\Core\Checkout\Promotion\Cart\PromotionItemBuilder"/>
+            <argument type="service" id="Shopware\Core\Framework\Util\HtmlSanitizer"/>
             <!-- // inject after product collector(5000) -->
             <tag name="shopware.cart.collector" priority="4900"/>
         </service>
 
         <service id="Shopware\Core\Checkout\Promotion\Cart\PromotionProcessor">
diff --git a/src/Core/Checkout/Promotion/Cart/PromotionCollector.php b/src/Core/Checkout/Promotion/Cart/PromotionCollector.php
index aa7de141ae7..ff895b4b2cf 100644
--- a/src/Core/Checkout/Promotion/Cart/PromotionCollector.php
+++ b/src/Core/Checkout/Promotion/Cart/PromotionCollector.php
@@ -20,10 +20,11 @@ use Shopware\Core\Checkout\Promotion\Gateway\Template\PermittedGlobalCodePromoti
 use Shopware\Core\Checkout\Promotion\Gateway\Template\PermittedIndividualCodePromotions;
 use Shopware\Core\Checkout\Promotion\PromotionCollection;
 use Shopware\Core\Checkout\Promotion\PromotionEntity;
 use Shopware\Core\Framework\DataAbstractionLayer\Exception\InconsistentCriteriaIdsException;
 use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
+use Shopware\Core\Framework\Util\HtmlSanitizer;
 use Shopware\Core\System\SalesChannel\SalesChannelContext;
 
 class PromotionCollector implements CartDataCollectorInterface
 {
     use PromotionCartInformationTrait;
@@ -33,16 +34,19 @@ class PromotionCollector implements CartDataCollectorInterface
 
     private PromotionGatewayInterface $gateway;
 
     private PromotionItemBuilder $itemBuilder;
 
+    private HtmlSanitizer $htmlSanitizer;
+
     private array $requiredDalAssociations;
 
-    public function __construct(PromotionGatewayInterface $gateway, PromotionItemBuilder $itemBuilder)
+    public function __construct(PromotionGatewayInterface $gateway, PromotionItemBuilder $itemBuilder, HtmlSanitizer $htmlSanitizer)
     {
         $this->gateway = $gateway;
         $this->itemBuilder = $itemBuilder;
+        $this->htmlSanitizer = $htmlSanitizer;
 
         $this->requiredDalAssociations = [
             'personaRules',
             'personaCustomers',
             'cartRules',
@@ -132,11 +136,12 @@ class PromotionCollector implements CartDataCollectorInterface
         // now iterate through all codes that have been added
         // and add errors, if a promotion for that code couldn't be found
         foreach ($allCodes as $code) {
             if (!\in_array($code, $foundCodes, true)) {
                 $cartExtension->removeCode($code);
-                $this->addPromotionNotFoundError($code, $original);
+
+                $this->addPromotionNotFoundError($this->htmlSanitizer->sanitize($code, null, true), $original);
             }
         }
 
         // if we do have promotions, set them to be processed
         // otherwise make sure to remove the entry to avoid any processing
diff --git a/src/Storefront/Controller/CartLineItemController.php b/src/Storefront/Controller/CartLineItemController.php
index ed6a964d942..779b657df6c 100644
--- a/src/Storefront/Controller/CartLineItemController.php
+++ b/src/Storefront/Controller/CartLineItemController.php
@@ -18,10 +18,11 @@ use Shopware\Core\Framework\DataAbstractionLayer\Exception\InconsistentCriteriaI
 use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
 use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
 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\Util\HtmlSanitizer;
 use Shopware\Core\Framework\Validation\DataBag\RequestDataBag;
 use Shopware\Core\System\SalesChannel\Entity\SalesChannelRepositoryInterface;
 use Shopware\Core\System\SalesChannel\SalesChannelContext;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\Response;
@@ -30,40 +31,32 @@ use Symfony\Component\Routing\Annotation\Route;
 /**
  * @RouteScope(scopes={"storefront"})
  */
 class CartLineItemController extends StorefrontController
 {
-    /**
-     * @var CartService
-     */
-    private $cartService;
+    private CartService $cartService;
 
-    /**
-     * @var PromotionItemBuilder
-     */
-    private $promotionItemBuilder;
+    private PromotionItemBuilder $promotionItemBuilder;
 
-    /**
-     * @var SalesChannelRepositoryInterface
-     */
-    private $productRepository;
+    private SalesChannelRepositoryInterface $productRepository;
 
-    /**
-     * @var ProductLineItemFactory
-     */
-    private $productLineItemFactory;
+    private ProductLineItemFactory $productLineItemFactory;
+
+    private HtmlSanitizer $htmlSanitizer;
 
     public function __construct(
         CartService $cartService,
         SalesChannelRepositoryInterface $productRepository,
         PromotionItemBuilder $promotionItemBuilder,
-        ProductLineItemFactory $productLineItemFactory
+        ProductLineItemFactory $productLineItemFactory,
+        HtmlSanitizer $htmlSanitizer
     ) {
         $this->cartService = $cartService;
         $this->productRepository = $productRepository;
         $this->promotionItemBuilder = $promotionItemBuilder;
         $this->productLineItemFactory = $productLineItemFactory;
+        $this->htmlSanitizer = $htmlSanitizer;
     }
 
     /**
      * @Since("6.0.0.0")
      * @Route("/checkout/line-item/delete/{id}", name="frontend.checkout.line-item.delete", methods={"POST", "DELETE"}, defaults={"XmlHttpRequest": true})
@@ -167,11 +160,11 @@ class CartLineItemController extends StorefrontController
      * @throws InconsistentCriteriaIdsException
      * @throws MissingRequestParameterException
      */
     public function addProductByNumber(Request $request, SalesChannelContext $salesChannelContext): Response
     {
-        $number = $request->request->get('number');
+        $number = (string) $request->request->get('number');
 
         if (!$number) {
             throw new MissingRequestParameterException('number');
         }
 
@@ -181,11 +174,14 @@ class CartLineItemController extends StorefrontController
 
         $idSearchResult = $this->productRepository->searchIds($criteria, $salesChannelContext);
         $data = $idSearchResult->getIds();
 
         if (empty($data)) {
-            $this->addFlash(self::DANGER, $this->trans('error.productNotFound', ['%number%' => $number]));
+            $this->addFlash(self::DANGER, $this->trans(
+                'error.productNotFound',
+                ['%number%' => $this->htmlSanitizer->sanitize($number, null, true)]
+            ));
 
             return $this->createActionResponse($request);
         }
 
         /** @var string $productId */
diff --git a/src/Storefront/DependencyInjection/controller.xml b/src/Storefront/DependencyInjection/controller.xml
index 32b1c627f8d..79b8ed05cc2 100644
--- a/src/Storefront/DependencyInjection/controller.xml
+++ b/src/Storefront/DependencyInjection/controller.xml
@@ -96,10 +96,11 @@
         <service id="Shopware\Storefront\Controller\CartLineItemController">
             <argument type="service" id="Shopware\Core\Checkout\Cart\SalesChannel\CartService"/>
             <argument type="service" id="sales_channel.product.repository"/>
             <argument type="service" id="Shopware\Core\Checkout\Promotion\Cart\PromotionItemBuilder"/>
             <argument type="service" id="Shopware\Core\Content\Product\Cart\ProductLineItemFactory"/>
+            <argument type="service" id="Shopware\Core\Framework\Util\HtmlSanitizer"/>
             <call method="setContainer">
                 <argument type="service" id="service_container"/>
             </call>
             <call method="setTwig">
                 <argument type="service" id="twig"/>
diff --git a/src/Storefront/Test/Controller/CartLineItemControllerTest.php b/src/Storefront/Test/Controller/CartLineItemControllerTest.php
index 88fa4f11e67..501e045e23d 100644
--- a/src/Storefront/Test/Controller/CartLineItemControllerTest.php
+++ b/src/Storefront/Test/Controller/CartLineItemControllerTest.php
@@ -36,25 +36,33 @@ class CartLineItemControllerTest extends TestCase
     }
 
     /**
      * @dataProvider productNumbers
      */
-    public function testAddProductByNumber(string $productId, string $productNumber): void
+    public function testAddProductByNumber(?string $productId, string $productNumber): void
     {
         $contextToken = Uuid::randomHex();
 
         $cartService = $this->getContainer()->get(CartService::class);
-        $this->createProduct($productId, $productNumber);
+        if ($productId) {
+            $this->createProduct($productId, $productNumber);
+        }
         $request = $this->createRequest(['number' => $productNumber]);
 
         $salesChannelContext = $this->createSalesChannelContext($contextToken);
         $response = $this->getContainer()->get(CartLineItemController::class)->addProductByNumber($request, $salesChannelContext);
 
         $cartLineItem = $cartService->getCart($contextToken, $salesChannelContext)->getLineItems()->get($productId);
-
-        static::assertArrayHasKey('success', $this->getFlashBag()->all());
-        static::assertNotNull($cartLineItem);
+        $flashBag = $this->getFlashBag()->all();
+        if ($productId) {
+            static::assertArrayHasKey('success', $flashBag);
+            static::assertNotNull($cartLineItem);
+        } else {
+            static::assertArrayHasKey('danger', $flashBag);
+            static::assertSame($this->getContainer()->get('translator')->trans('error.productNotFound', ['%number%' => \strip_tags($productNumber)]), $flashBag['danger'][0]);
+            static::assertNull($cartLineItem);
+        }
         static::assertSame(200, $response->getStatusCode());
     }
 
     public function productNumbers(): array
     {
@@ -63,13 +71,46 @@ class CartLineItemControllerTest extends TestCase
             [Uuid::randomHex(), 'test 123'],
             [Uuid::randomHex(), 'test-123'],
             [Uuid::randomHex(), 'test_123'],
             [Uuid::randomHex(), 'testäüö123'],
             [Uuid::randomHex(), 'test/123'],
+            [null, 'nonExisting'],
+            [null, 'with<br>HTML'],
+        ];
+    }
+
+    public function promotions(): array
+    {
+        return [
+            ['testCode'],
+            ['with<br>HTML'],
         ];
     }
 
+    /**
+     * @dataProvider promotions
+     */
+    public function testAddPromotion(string $code): void
+    {
+        $contextToken = Uuid::randomHex();
+
+        $cartService = $this->getContainer()->get(CartService::class);
+        $request = $this->createRequest(['code' => $code]);
+
+        $salesChannelContext = $this->createSalesChannelContext($contextToken);
+        $this->getContainer()->get(CartLineItemController::class)->addPromotion(
+            $cartService->getCart($contextToken, $salesChannelContext),
+            $request,
+            $salesChannelContext
+        );
+
+        $flashBag = $this->getFlashBag()->all();
+        static::assertArrayHasKey('danger', $flashBag);
+        static::assertSame($this->getContainer()->get('translator')->trans('checkout.promotion-not-found', ['%code%' => \strip_tags($code)]), $flashBag['danger'][0]);
+        static::assertSame(0, $cartService->getCart($contextToken, $salesChannelContext)->getLineItems()->count());
+    }
+
     private function getLineItemAddPayload(string $productId): array
     {
         return [
             'redirectTo' => 'frontend.cart.offcanvas',
             'lineItems' => [

Vulnerable code:         $uniqueKey = 'promotion-' . $data['referencedId'];
        $item->setReferencedId($data['referencedId']);
    public function __construct(PromotionGatewayInterface $gateway, PromotionItemBuilder $itemBuilder)
                $this->addPromotionNotFoundError($code, $original);
    /**
     * @var CartService
     */
    private $cartService;
    /**
     * @var PromotionItemBuilder
     */
    private $promotionItemBuilder;
    /**
     * @var SalesChannelRepositoryInterface
     */
    private $productRepository;
    /**
     * @var ProductLineItemFactory
     */
    private $productLineItemFactory;
        ProductLineItemFactory $productLineItemFactory
        $number = $request->request->get('number');
            $this->addFlash(self::DANGER, $this->trans('error.productNotFound', ['%number%' => $number]));
    public function testAddProductByNumber(string $productId, string $productNumber): void
        $this->createProduct($productId, $productNumber);

        static::assertArrayHasKey('success', $this->getFlashBag()->all());
        static::assertNotNull($cartLineItem);
Fixed code: ---
title: Add filtering to product and promotion codes
issue: NEXT-19276
author: Max Stegmeyer
author_email: m.stegmeyer@shopware.com
---
# Storefront
* Changed `Shopware\Storefront\Controller\CartLineItemController` to sanitize from promotion codes and product number error messages
        $code = $data['referencedId'];

        $uniqueKey = 'promotion-' . $code;
        $item->setReferencedId($code);
            <argument type="service" id="Shopware\Core\Framework\Util\HtmlSanitizer"/>
use Shopware\Core\Framework\Util\HtmlSanitizer;
    private HtmlSanitizer $htmlSanitizer;

    public function __construct(PromotionGatewayInterface $gateway, PromotionItemBuilder $itemBuilder, HtmlSanitizer $htmlSanitizer)
        $this->htmlSanitizer = $htmlSanitizer;

                $this->addPromotionNotFoundError($this->htmlSanitizer->sanitize($code, null, true), $original);
use Shopware\Core\Framework\Util\HtmlSanitizer;
    private CartService $cartService;
    private PromotionItemBuilder $promotionItemBuilder;
    private SalesChannelRepositoryInterface $productRepository;
    private ProductLineItemFactory $productLineItemFactory;

    private HtmlSanitizer $htmlSanitizer;
        ProductLineItemFactory $productLineItemFactory,
        HtmlSanitizer $htmlSanitizer
        $this->htmlSanitizer = $htmlSanitizer;
        $number = (string) $request->request->get('number');
            $this->addFlash(self::DANGER, $this->trans(
                'error.productNotFound',
                ['%number%' => $this->htmlSanitizer->sanitize($number, null, true)]
            ));
            <argument type="service" id="Shopware\Core\Framework\Util\HtmlSanitizer"/>
    public function testAddProductByNumber(?string $productId, string $productNumber): void
        if ($productId) {
            $this->createProduct($productId, $productNumber);
        }
        $flashBag = $this->getFlashBag()->all();
        if ($productId) {
            static::assertArrayHasKey('success', $flashBag);
            static::assertNotNull($cartLineItem);
        } else {
            static::assertArrayHasKey('danger', $flashBag);
            static::assertSame($this->getContainer()->get('translator')->trans('error.productNotFound', ['%number%' => \strip_tags($productNumber)]), $flashBag['danger'][0]);
            static::assertNull($cartLineItem);
        }
            [null, 'nonExisting'],
            [null, 'with<br>HTML'],
        ];
    }

    public function promotions(): array
    {
        return [
            ['testCode'],
            ['with<br>HTML'],
    /**
     * @dataProvider promotions
     */
    public function testAddPromotion(string $code): void
    {
        $contextToken = Uuid::randomHex();

        $cartService = $this->getContainer()->get(CartService::class);
        $request = $this->createRequest(['code' => $code]);

        $salesChannelContext = $this->createSalesChannelContext($contextToken);
        $this->getContainer()->get(CartLineItemController::class)->addPromotion(
            $cartService->getCart($contextToken, $salesChannelContext),
            $request,
            $salesChannelContext
        );

        $flashBag = $this->getFlashBag()->all();
        static::assertArrayHasKey('danger', $flashBag);
        static::assertSame($this->getContainer()->get('translator')->trans('checkout.promotion-not-found', ['%code%' => \strip_tags($code)]), $flashBag['danger'][0]);
        static::assertSame(0, $cartService->getCart($contextToken, $salesChannelContext)->getLineItems()->count());
    }