Email verification bypass in Sylius
Description
Sylius users can change their verified email without re-verification, potentially leading to accounts with mismatched, verified email addresses.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Sylius users can change their verified email without re-verification, potentially leading to accounts with mismatched, verified email addresses.
Vulnerability
Overview
CVE-2020-15245 is an improper verification of email change in Sylius, an open-source eCommerce framework on Symfony. The flaw allows a registered user who has verified their initial email address (e.g., mail@example.com) to subsequently change that email to a completely different address (e.g., another@domain.com) without being required to re-verify the new address. As a result, the account remains in a verified and enabled state despite now being associated with an email that was never actually verified by its owner [1].
Exploitation
Scenario
Exploitation requires only a standard registered user account in the affected Sylius shop. The attacker first registers and verifies an account with any email they control. Then, using the account settings, they change the email to a target address (which they do not control). Because the verification status is not reset on email change, the account now appears verified under the new email. Notably, this does not allow taking over an existing guest or customer account; it only enables an attacker to maintain a verified account under a falsified email [1]. The root cause is that the sylius.customer.pre_update event does not automatically disable or unverify the user when the email changes [2].
Impact
The primary impact is a loss of integrity in user identity. An attacker can maintain a verified account under a fraudulent email address, which could be used to receive order notifications, access account history, or conduct other shop activities under a false identity. This undermines the trust model that relies on email verification as proof of user identity [1].
Mitigation
The vulnerability is patched in Sylius versions 1.6.9, 1.7.9, and 1.8.3 [1]. As a workaround, administrators can implement a custom event listener for sylius.customer.pre_update that detects email changes (by comparing customer email and user username) and disables or unverifies the user accordingly, being careful to exclude admin users [1].
AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
sylius/syliusPackagist | >= 1.7.0, < 1.7.9 | 1.7.9 |
sylius/syliusPackagist | >= 1.8.0, < 1.8.3 | 1.8.3 |
sylius/syliusPackagist | >= 1.0.0, < 1.6.9 | 1.6.9 |
Affected products
2Patches
160636d711a40[Shop] Disabling customer when email has been changed
4 files changed · +291 −1
features/account/customer_account/editing_customer_profile.feature+13 −1 modified@@ -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"
src/Sylius/Behat/Context/Ui/Shop/VerificationContext.php+163 −0 added@@ -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() + ); + } +}
src/Sylius/Bundle/ShopBundle/EventListener/CustomerEmailUpdaterListener.php+106 −0 added@@ -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); + } +}
src/Sylius/Bundle/ShopBundle/Resources/config/services.xml+9 −0 modified@@ -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" />
Vulnerability mechanics
Root cause
"Missing verification reset when a user changes their email address after already being verified."
Attack vector
An attacker registers a new account with email `mail@example.com`, completes email verification, and then changes the account email to `another@domain.com`. Because the system did not reset the verification status when the email changed, the account remains verified and enabled under the new, unverified email address. This allows the attacker to hold a verified account tied to an email they may not control, though it does not enable takeover of existing accounts [CWE-862].
Affected code
The vulnerability exists in the customer email update flow. The patch introduces `src/Sylius/Bundle/ShopBundle/EventListener/CustomerEmailUpdaterListener.php` with two methods: `eraseVerification` (listening to `sylius.customer.pre_update`) and `sendVerificationEmail` (listening to `sylius.customer.post_update`). The corresponding service registration is added in `src/Sylius/Bundle/ShopBundle/Resources/config/services.xml`.
What the fix does
The patch adds a `CustomerEmailUpdaterListener` that hooks into the `sylius.customer.pre_update` event. In `eraseVerification`, when the customer's email differs from the user's username (indicating an email change), the listener sets `verifiedAt` to null, generates a new verification token, and — if the channel requires account verification — disables the user account. The `sendVerificationEmail` method then dispatches a new verification email if the user is disabled, unverified, and has a token. This ensures that changing an email address invalidates the previous verification and forces re-verification.
Preconditions
- configThe shop must allow users to change their email address after registration
- authThe attacker must have a registered and verified account
- inputThe attacker must be able to submit a profile update with a new email address
Generated on May 23, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-6gw4-x63h-5499ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2020-15245ghsaADVISORY
- github.com/FriendsOfPHP/security-advisories/blob/master/sylius/sylius/CVE-2020-15245.yamlghsaWEB
- github.com/Sylius/Sylius/commit/60636d711a4011e8694d10d201b53632c7e8ecafghsax_refsource_MISCWEB
- github.com/Sylius/Sylius/security/advisories/GHSA-6gw4-x63h-5499ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.