CVE-2020-15245 (published 2020-10-19T21:15Z)

In Sylius before versions 1.6.9, 1.7.9 and 1.8.3, the user may register in a shop by email mail@example.com, verify it, change it to the mail another@domain.com and stay verified and enabled. This may lead to having accounts addressed to totally different emails, that were verified. Note, that this way one is not able to take over any existing account (guest or normal one). The issue has been patched in Sylius 1.6.9, 1.7.9 and 1.8.3. As a workaround, you may resolve this issue on your own by creating a custom event listener, which will listen to the sylius.customer.pre_update event. You can determine that email has been changed if customer email and user username are different. They are synchronized later on. Pay attention, to email changing behavior for administrators. You may need to skip this logic for them. In order to achieve this, you should either check master request path info, if it does not contain /admin prefix or adjust event triggered during customer update in the shop. You can find more information on how to customize the event here.

Severity: MEDIUM
CVSS v2 base score: 4.0 (AV:N/AC:L/Au:S/C:N/I:P/A:N)
CVSS v3 base score: 4.3, MEDIUM (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:N)

Weakness information:
cwe_id: CWE-862
cwe_name: Missing Authorization
description: The product does not perform an authorization check when an actor attempts to access a resource or perform an action.
extended_description: 
            Assuming a user with a given identity, authorization is the process of determining whether that user can access a given resource, based on the user's privileges and any permissions or other access-control specifications that apply to the resource.
            When access control checks are not applied, users are able to access data or perform actions that they should not be allowed to perform. This can lead to a wide range of problems, including information exposures, denial of service, and arbitrary code execution.
         
         

Fix information:
repo_url: https://github.com/Sylius/Sylius
commit_details: repo_url: https://github.com/Sylius/Sylius
author: Łukasz Chruściel
author_date: 2020-10-06 17:09:15+02:00
msg: [Shop] Disabling customer when email has been changed
repository: repo_url: https://github.com/Sylius/Sylius
repo_name: Sylius/Sylius
description: Open Source eCommerce Framework on Symfony
repo_language: PHP
owner: Sylius
file_changes: filename: editing_customer_profile.feature
change_type: ModificationType.MODIFY
diff: @@ -19,9 +19,21 @@ Feature: Editing a customer profile
         And my name should be "Will Conway"
 
     @ui
-    Scenario: Changing my email
+    Scenario: Changing my email if channel requires verification
         When I want to modify my profile
         And I specify the customer email as "frank@underwood.com"
         And I save my changes
         Then I should be notified that it has been successfully edited
+        And I should be notified that the verification email has been sent
+        And it should be sent to "frank@underwood.com"
+        And I should not be logged in
+
+    @ui
+    Scenario: Changing my email if channel does not require verification
+        Given "United States" channel has account verification disabled
+        When I want to modify my profile
+        And I specify the customer email as "frank@underwood.com"
+        And I save my changes
+        Then I should be notified that it has been successfully edited
+        And my account should not be verified
         And my email should be "frank@underwood.com"

programming_language: Ruby
filename: VerificationContext.php
change_type: ModificationType.ADD
diff: @@ -0,0 +1,163 @@
+<?php
+
+/*
+ * This file is part of the Sylius package.
+ *
+ * (c) Paweł Jędrzejewski
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Sylius\Behat\Context\Ui\Shop;
+
+use Behat\Behat\Context\Context;
+use Sylius\Behat\NotificationType;
+use Sylius\Behat\Page\Shop\Account\DashboardPageInterface;
+use Sylius\Behat\Page\Shop\Account\VerificationPageInterface;
+use Sylius\Behat\Service\NotificationCheckerInterface;
+use Sylius\Behat\Service\SharedStorageInterface;
+use Sylius\Component\Core\Model\CustomerInterface;
+use Sylius\Component\Core\Model\ShopUserInterface;
+use Webmozart\Assert\Assert;
+
+class VerificationContext implements Context
+{
+    /** @var SharedStorageInterface */
+    private $sharedStorage;
+
+    /** @var DashboardPageInterface */
+    private $dashboardPage;
+
+    /** @var VerificationPageInterface */
+    private $verificationPage;
+
+    /** @var NotificationCheckerInterface */
+    private $notificationChecker;
+
+    public function __construct(
+        SharedStorageInterface $sharedStorage,
+        DashboardPageInterface $dashboardPage,
+        VerificationPageInterface $verificationPage,
+        NotificationCheckerInterface $notificationChecker
+    ) {
+        $this->sharedStorage = $sharedStorage;
+        $this->dashboardPage = $dashboardPage;
+        $this->verificationPage = $verificationPage;
+        $this->notificationChecker = $notificationChecker;
+    }
+
+    /**
+     * @Then I should be notified that my account has been created and the verification email has been sent
+     */
+    public function iShouldBeNotifiedThatNewAccountHasBeenSuccessfullyCreated(): void
+    {
+        $this->notificationChecker->checkNotification(
+            'Thank you for registering, check your email to verify your account.',
+            NotificationType::success()
+        );
+    }
+
+    /**
+     * @Then /^my account should be verified$/
+     */
+    public function myAccountShouldBeVerified(): void
+    {
+        Assert::true($this->dashboardPage->isVerified());
+    }
+
+    /**
+     * @When /^(I) try to verify my account using the link from this email$/
+     */
+    public function iUseItToVerify(ShopUserInterface $user): void
+    {
+        $this->verificationPage->verifyAccount($user->getEmailVerificationToken());
+    }
+
+    /**
+     * @When I verify my account using link sent to :customer
+     */
+    public function iVerifyMyAccount(CustomerInterface $customer): void
+    {
+        $user = $customer->getUser();
+        Assert::notNull($user, 'No account for given customer');
+
+        $this->iUseItToVerify($user);
+    }
+
+    /**
+     * @When I resend the verification email
+     */
+    public function iResendVerificationEmail(): void
+    {
+        $this->dashboardPage->open();
+        $this->dashboardPage->pressResendVerificationEmail();
+    }
+
+    /**
+     * @When I use the verification link from the first email to verify
+     */
+    public function iUseVerificationLinkFromFirstEmailToVerify(): void
+    {
+        $token = $this->sharedStorage->get('verification_token');
+
+        $this->verificationPage->verifyAccount($token);
+    }
+
+    /**
+     * @When I (try to )verify using :token token
+     */
+    public function iTryToVerifyUsing(string $token): void
+    {
+        $this->verificationPage->verifyAccount($token);
+    }
+
+    /**
+     * @Then /^(?:my|his|her) account should not be verified$/
+     */
+    public function myAccountShouldNotBeVerified(): void
+    {
+        $this->dashboardPage->open();
+
+        Assert::false($this->dashboardPage->isVerified());
+    }
+
+    /**
+     * @Then I should not be able to resend the verification email
+     */
+    public function iShouldBeUnableToResendVerificationEmail(): void
+    {
+        $this->dashboardPage->open();
+
+        Assert::false($this->dashboardPage->hasResendVerificationEmailButton());
+    }
+
+    /**
+     * @Then I should be notified that the verification was successful
+     */
+    public function iShouldBeNotifiedThatTheVerificationWasSuccessful(): void
+    {
+        $this->notificationChecker->checkNotification('has been successfully verified.', NotificationType::success());
+    }
+
+    /**
+     * @Then I should be notified that the verification token is invalid
+     */
+    public function iShouldBeNotifiedThatTheVerificationTokenIsInvalid(): void
+    {
+        $this->notificationChecker->checkNotification('The verification token is invalid.', NotificationType::failure());
+    }
+
+    /**
+     * @Then I should be notified that the verification email has been sent
+     */
+    public function iShouldBeNotifiedThatTheVerificationEmailHasBeenSent(): void
+    {
+        $this->notificationChecker->checkNotification(
+            'An email with the verification link has been sent to your email address.',
+            NotificationType::success()
+        );
+    }
+}

programming_language: PHP
filename: CustomerEmailUpdaterListener.php
change_type: ModificationType.ADD
diff: @@ -0,0 +1,106 @@
+<?php
+
+/*
+ * This file is part of the Sylius package.
+ *
+ * (c) Paweł Jędrzejewski
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Sylius\Bundle\ShopBundle\EventListener;
+
+use Sylius\Bundle\UserBundle\EventListener\PasswordUpdaterListener as BasePasswordUpdaterListener;
+use Sylius\Bundle\UserBundle\UserEvents;
+use Sylius\Component\Channel\Context\ChannelContextInterface;
+use Sylius\Component\Core\Model\ChannelInterface;
+use Sylius\Component\Core\Model\CustomerInterface;
+use Sylius\Component\Core\Model\ShopUserInterface;
+use Sylius\Component\User\Security\Generator\GeneratorInterface;
+use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+use Symfony\Component\EventDispatcher\GenericEvent;
+use Symfony\Component\HttpFoundation\Session\Flash\FlashBagInterface;
+use Symfony\Component\HttpFoundation\Session\SessionInterface;
+use Webmozart\Assert\Assert;
+
+final class CustomerEmailUpdaterListener
+{
+    /** @var GeneratorInterface */
+    private $tokenGenerator;
+    /** @var ChannelContextInterface */
+    private $channelContext;
+    /** @var EventDispatcherInterface */
+    private $eventDispatcher;
+    /** @var SessionInterface */
+    private $session;
+
+    public function __construct(
+        GeneratorInterface $tokenGenerator,
+        ChannelContextInterface $channelContext,
+        EventDispatcherInterface $eventDispatcher,
+        SessionInterface $session
+    ) {
+        $this->tokenGenerator = $tokenGenerator;
+        $this->channelContext = $channelContext;
+        $this->eventDispatcher = $eventDispatcher;
+        $this->session = $session;
+    }
+
+    public function eraseVerification(GenericEvent $event): void
+    {
+        $customer = $event->getSubject();
+
+        /** @var CustomerInterface $customer */
+        Assert::isInstanceOf($customer, CustomerInterface::class);
+
+        /** @var ShopUserInterface $user */
+        $user = $customer->getUser();
+        if ($customer->getEmail() !== $user->getUsername()) {
+            $user->setEmail($customer->getEmail());
+            $user->setVerifiedAt(null);
+
+            $token = $this->tokenGenerator->generate();
+            $user->setEmailVerificationToken($token);
+
+            /** @var ChannelInterface $channel */
+            $channel = $this->channelContext->getChannel();
+
+            if ($channel->isAccountVerificationRequired()) {
+                $user->setEnabled(false);
+            }
+        }
+    }
+
+    public function sendVerificationEmail(GenericEvent $event): void
+    {
+        /** @var ChannelInterface $channel */
+        $channel = $this->channelContext->getChannel();
+
+        if (!$channel->isAccountVerificationRequired()) {
+            return;
+        }
+
+        $customer = $event->getSubject();
+
+        /** @var CustomerInterface $customer */
+        Assert::isInstanceOf($customer, CustomerInterface::class);
+
+        /** @var ShopUserInterface $user */
+        $user = $customer->getUser();
+
+        if (!$user->isEnabled() && !$user->isVerified() && null !== $user->getEmailVerificationToken()) {
+            $this->eventDispatcher->dispatch(UserEvents::REQUEST_VERIFICATION_TOKEN, new GenericEvent($user));
+            $this->addFlash('success', 'sylius.user.verify_email_request');
+        }
+    }
+
+    private function addFlash(string $type, string $message): void
+    {
+        /** @var FlashBagInterface $flashBag */
+        $flashBag = $this->session->getBag('flashes');
+        $flashBag->add($type, $message);
+    }
+}

programming_language: PHP
filename: services.xml
change_type: ModificationType.MODIFY
diff: @@ -29,6 +29,15 @@
             <argument type="service" id="sylius.storage.cart_session" />
         </service>
 
+        <service id="sylius.listener.email_updater" class="Sylius\Bundle\ShopBundle\EventListener\CustomerEmailUpdaterListener">
+            <argument type="service" id="sylius.shop_user.token_generator.email_verification" />
+            <argument type="service" id="sylius.context.channel" />
+            <argument type="service" id="event_dispatcher" />
+            <argument type="service" id="session" />
+            <tag name="kernel.event_listener" event="sylius.customer.pre_update" method="eraseVerification" />
+            <tag name="kernel.event_listener" event="sylius.customer.post_update" method="sendVerificationEmail" />
+        </service>
+
         <service id="sylius.context.cart.session_and_channel_based" class="Sylius\Bundle\CoreBundle\Context\SessionAndChannelBasedCartContext">
             <argument type="service" id="sylius.storage.cart_session" />
             <argument type="service" id="sylius.context.channel" />

programming_language: TypeScript