CVE-2020-15245 (PHP, published 2020-10-19T21:15Z)
Description: value: 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 v3 base score: 4.3)
Weakness: CWE-862 \u2014 Missing Authorization
The product does not perform an authorization check when an actor attempts to access a resource or perform an action.
Repository: https://github.com/Sylius/Sylius
Files changed:
features/account/customer_account/editing_customer_profile.feature
src/Sylius/Behat/Context/Ui/Shop/VerificationContext.php
src/Sylius/Bundle/ShopBundle/EventListener/CustomerEmailUpdaterListener.php
src/Sylius/Bundle/ShopBundle/Resources/config/services.xml
Commit message: [Shop] Disabling customer when email has been changed


Vulnerable code:
    Scenario: Changing my email

Fixed code:
    Scenario: Changing my email if channel requires verification
        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
<?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()
        );
    }
}
<?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);
    }
}
        <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>