Critical severityOSV Advisory· Published Apr 17, 2019· Updated Aug 4, 2024
CVE-2019-10643
CVE-2019-10643
Description
Contao 4.7 allows Use of a Key Past its Expiration Date.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
contao/contaoPackagist | >= 4.7.0, < 4.7.3 | 4.7.3 |
contao/core-bundlePackagist | >= 4.7.0, < 4.7.3 | 4.7.3 |
Affected products
1Patches
170348cc812b1Invalidate old opt-in tokens when a token is confirmed (see CVE-2019-10643)
14 files changed · +353 −34
CHANGELOG.md+1 −0 modified@@ -2,6 +2,7 @@ ## DEV + * Invalidate old opt-in tokens when a token is confirmed (see CVE-2019-10643). * Invalidate the user sessions if a password changes (see CVE-2019-10641). * Correctly check if a file or folder is excluded from synchronization (see 410).
comments-bundle/src/Resources/contao/classes/Comments.php+18 −4 modified@@ -584,7 +584,7 @@ public static function addCommentsSubscription(CommentsModel $objComment) /** @var OptIn $optIn */ $optIn = System::getContainer()->get('contao.opt-in'); - $optInToken = $optIn->create('com-', $objComment->email, array('tl_comments_notify'=>array($objNotify->id))); + $optInToken = $optIn->create('com', $objComment->email, array('tl_comments_notify'=>array($objNotify->id))); // Send the token $optInToken->send(sprintf($GLOBALS['TL_LANG']['MSC']['com_optInSubject'], Idna::decode(Environment::get('host'))), sprintf($GLOBALS['TL_LANG']['MSC']['com_optInMessage'], $objComment->name, $strUrl, $strUrl . $strConnector . 'token=' . $optInToken->getIdentifier(), $strUrl . $strConnector . 'token=' . $objNotify->tokenRemove)); @@ -603,9 +603,23 @@ public static function changeSubscriptionStatus(FrontendTemplate $objTemplate) $optIn = System::getContainer()->get('contao.opt-in'); // Find an unconfirmed token with only one related record - if ((!$optInToken = $optIn->find(Input::get('token'))) || $optInToken->isConfirmed() || \count($arrRelated = $optInToken->getRelatedRecords()) != 1 || key($arrRelated) != 'tl_comments_notify' || \count($arrIds = current($arrRelated)) != 1 || (!$objNotify = CommentsNotifyModel::findByPk($arrIds[0]))) + if ((!$optInToken = $optIn->find(Input::get('token'))) || !$optInToken->isValid() || \count($arrRelated = $optInToken->getRelatedRecords()) != 1 || key($arrRelated) != 'tl_comments_notify' || \count($arrIds = current($arrRelated)) != 1 || (!$objNotify = CommentsNotifyModel::findByPk($arrIds[0]))) { - $objTemplate->confirm = $GLOBALS['TL_LANG']['MSC']['invalidTokenUrl']; + $objTemplate->confirm = $GLOBALS['TL_LANG']['MSC']['invalidToken']; + + return; + } + + if ($optInToken->isConfirmed()) + { + $objTemplate->confirm = $GLOBALS['TL_LANG']['MSC']['tokenConfirmed']; + + return; + } + + if ($optInToken->getEmail() != $objNotify->email) + { + $objTemplate->confirm = $GLOBALS['TL_LANG']['MSC']['tokenEmailMismatch']; return; } @@ -623,7 +637,7 @@ public static function changeSubscriptionStatus(FrontendTemplate $objTemplate) if ($objNotify === null) { - $objTemplate->confirm = $GLOBALS['TL_LANG']['MSC']['invalidTokenUrl']; + $objTemplate->confirm = $GLOBALS['TL_LANG']['MSC']['invalidToken']; return; }
core-bundle/src/OptIn/OptIn.php+5 −1 modified@@ -33,14 +33,18 @@ public function __construct(ContaoFramework $framework) */ public function create(string $prefix, string $email, array $related): OptInTokenInterface { + if ($prefix) { + $prefix = rtrim($prefix, '-'); + } + if (\strlen($prefix) > 6) { throw new \InvalidArgumentException('The token prefix must not be longer than 6 characters'); } $token = bin2hex(random_bytes(12)); if ($prefix) { - $token = $prefix.substr($token, \strlen($prefix)); + $token = $prefix.'-'.substr($token, \strlen($prefix) + 1); } /** @var OptInModel $optIn */
core-bundle/src/OptIn/OptInToken.php+39 −1 modified@@ -55,7 +55,7 @@ public function getEmail(): string */ public function isValid(): bool { - return $this->model->createdOn > strtotime('-24 hours'); + return !$this->model->invalidatedThrough && $this->model->createdOn > strtotime('-24 hours'); } /** @@ -75,6 +75,44 @@ public function confirm(): void $this->model->confirmedOn = time(); $this->model->removeOn = strtotime('+3 years'); $this->model->save(); + + $related = $this->model->getRelatedRecords(); + + if (empty($related)) { + return; + } + + /** @var OptInModel $adapter */ + $adapter = $this->framework->getAdapter(OptInModel::class); + $prefix = strtok($this->getIdentifier(), '-'); + + // Invalidate other tokens that relate to the same records + foreach ($related as $table => $ids) { + if (!$models = $adapter->findByRelatedTableAndIds($table, $ids)) { + continue; + } + + foreach ($models as $model) { + if ( + $model->confirmedOn > 0 + || $model->invalidatedThrough + || $model->token === $this->getIdentifier() + || 0 !== strncmp($model->token, $prefix.'-', \strlen($prefix) + 1) + ) { + continue; + } + + $token = new OptInToken($model, $this->framework); + + // The related records must match exactly + if ($token->getRelatedRecords() !== $related) { + continue; + } + + $model->invalidatedThrough = $this->model->token; + $model->save(); + } + } } /**
core-bundle/src/Resources/contao/controllers/BackendConfirm.php+3 −3 modified@@ -145,14 +145,14 @@ public function run() $objTemplate->link = StringUtil::specialchars($url); $objTemplate->info = $arrInfo; $objTemplate->labels = $GLOBALS['TL_LANG']['CONFIRM']; - $objTemplate->explain = $GLOBALS['TL_LANG']['ERR']['invalidTokenUrl']; + $objTemplate->explain = $GLOBALS['TL_LANG']['MSC']['invalidTokenUrl']; $objTemplate->cancel = $GLOBALS['TL_LANG']['MSC']['cancelBT']; $objTemplate->continue = $GLOBALS['TL_LANG']['MSC']['continue']; $objTemplate->theme = Backend::getTheme(); $objTemplate->base = Environment::get('base'); $objTemplate->language = $GLOBALS['TL_LANGUAGE']; - $objTemplate->h1 = $GLOBALS['TL_LANG']['MSC']['invalidTokenUrl']; - $objTemplate->title = StringUtil::specialchars($GLOBALS['TL_LANG']['MSC']['invalidTokenUrl']); + $objTemplate->h1 = $GLOBALS['TL_LANG']['MSC']['invalidToken']; + $objTemplate->title = StringUtil::specialchars($GLOBALS['TL_LANG']['MSC']['invalidToken']); $objTemplate->host = Environment::get('host'); $objTemplate->charset = Config::get('characterSet');
core-bundle/src/Resources/contao/dca/tl_opt_in.php+7 −1 modified@@ -110,6 +110,12 @@ 'eval' => array('rgxp'=>'datim'), 'sql' => "int(10) unsigned NOT NULL default '0'" ), + 'invalidatedThrough' => array + ( + 'label' => &$GLOBALS['TL_LANG']['tl_opt_in']['invalidatedThrough'], + 'search' => true, + 'sql' => "varchar(24) NOT NULL default ''" + ), 'email' => array ( 'label' => &$GLOBALS['TL_LANG']['MSC']['emailAddress'], @@ -201,6 +207,6 @@ public function resendToken(Contao\DataContainer $dc) */ public function resendButton($row, $href, $label, $title, $icon, $attributes) { - return (!$row['confirmedOn'] && $row['emailSubject'] && $row['emailText'] && $row['createdOn'] > strtotime('-24 hours')) ? '<a href="'.$this->addToUrl($href.'&id='.$row['id']).'" title="'.Contao\StringUtil::specialchars($title).'"'.$attributes.'>'.Contao\Image::getHtml($icon, $label).'</a> ' : ''; + return (!$row['confirmedOn'] &&!$row['invalidatedThrough'] && $row['emailSubject'] && $row['emailText'] && $row['createdOn'] > strtotime('-24 hours')) ? '<a href="'.$this->addToUrl($href.'&id='.$row['id']).'" title="'.Contao\StringUtil::specialchars($title).'"'.$attributes.'>'.Contao\Image::getHtml($icon, $label).'</a> ' : ''; } }
core-bundle/src/Resources/contao/languages/en/default.xlf+10 −4 modified@@ -188,9 +188,6 @@ <trans-unit id="ERR.topLevelRegular"> <source>There are top-level pages in the site structure which are not website root pages.</source> </trans-unit> - <trans-unit id="ERR.invalidTokenUrl"> - <source>The link you were trying to open could not be verified. If you have clicked the link yourself or have received it by a trustworthy person, you can confirm the process below.</source> - </trans-unit> <trans-unit id="ERR.form"> <source>The form could not be sent</source> </trans-unit> @@ -1736,12 +1733,21 @@ <trans-unit id="MSC.relevance"> <source>%s relevance</source> </trans-unit> - <trans-unit id="MSC.invalidTokenUrl"> + <trans-unit id="MSC.invalidToken"> <source>Invalid token</source> </trans-unit> + <trans-unit id="MSC.tokenConfirmed"> + <source>This token has already been confirmed</source> + </trans-unit> + <trans-unit id="MSC.tokenEmailMismatch"> + <source>The e-mail address of the token does not match</source> + </trans-unit> <trans-unit id="MSC.resendToken"> <source>The token has been re-sent to %s.</source> </trans-unit> + <trans-unit id="MSC.invalidTokenUrl"> + <source>The link you were trying to open could not be verified. If you have clicked the link yourself or have received it by a trustworthy person, you can confirm the process below.</source> + </trans-unit> <trans-unit id="MSC.versionConflict"> <source>Version conflict</source> </trans-unit>
core-bundle/src/Resources/contao/languages/en/tl_opt_in.xlf+3 −0 modified@@ -11,6 +11,9 @@ <trans-unit id="tl_opt_in.removeOn"> <source>Remove on</source> </trans-unit> + <trans-unit id="tl_opt_in.invalidatedThrough"> + <source>Invalidated through</source> + </trans-unit> <trans-unit id="tl_opt_in.emailSubject"> <source>E-mail subject</source> </trans-unit>
core-bundle/src/Resources/contao/models/OptInModel.php+52 −2 modified@@ -22,6 +22,7 @@ * @property integer $createdOn * @property integer $confirmedOn * @property integer $removeOn + * @property string $invalidatedThrough * @property string $email * @property string $emailSubject * @property string $emailText @@ -35,6 +36,7 @@ * @method static OptInModel|null findOneByCreatedOn($val, array $opt=array()) * @method static OptInModel|null findOneByConfirmedOn($val, array $opt=array()) * @method static OptInModel|null findOneByRemoveOn($val, array $opt=array()) + * @method static OptInModel|null findOneByInvalidatedThrough($val, array $opt=array()) * @method static OptInModel|null findOneByEmail($val, array $opt=array()) * @method static OptInModel|null findOneByEmailSubject($val, array $opt=array()) * @method static OptInModel|null findOneByEmailText($val, array $opt=array()) @@ -44,6 +46,7 @@ * @method static Collection|OptInModel[]|OptInModel|null findByCreatedOn($val, array $opt=array()) * @method static Collection|OptInModel[]|OptInModel|null findByConfirmedOn($val, array $opt=array()) * @method static Collection|OptInModel[]|OptInModel|null findByRemoveOn($val, array $opt=array()) + * @method static Collection|OptInModel[]|OptInModel|null findByInvalidatedThrough($val, array $opt=array()) * @method static Collection|OptInModel[]|OptInModel|null findByEmail($val, array $opt=array()) * @method static Collection|OptInModel[]|OptInModel|null findByEmailSubject($val, array $opt=array()) * @method static Collection|OptInModel[]|OptInModel|null findByEmailText($val, array $opt=array()) @@ -57,6 +60,7 @@ * @method static integer countByCreatedOn($val, array $opt=array()) * @method static integer countByConfirmedOn($val, array $opt=array()) * @method static integer countByRemoveOn($val, array $opt=array()) + * @method static integer countByInvalidatedThrough($val, array $opt=array()) * @method static integer countByEmail($val, array $opt=array()) * @method static integer countByEmailSubject($val, array $opt=array()) * @method static integer countByEmailText($val, array $opt=array()) @@ -75,9 +79,9 @@ class OptInModel extends Model /** * Find expired double opt-in tokens * - * @param array $arrOptions An optional options array + * @param array $arrOptions * - * @return Collection|OptInModel[]|OptInModel|null A collection of models or null if there are no expired tokens + * @return Collection|OptInModel[]|OptInModel|null */ public static function findExpiredTokens(array $arrOptions=array()) { @@ -94,9 +98,14 @@ public static function findExpiredTokens(array $arrOptions=array()) * @param array $arrOptions * * @return static|null + * + * @deprecated Deprecated since Contao 4.7, to be removed in Contao 5.0; use the + * Contao\OptInModel::findByRelatedTableAndIds() method instead */ public static function findOneByRelatedTableAndId($strTable, $intId, array $arrOptions=array()) { + @trigger_error('Using the Contao\OptInModel::findOneByRelatedTableAndIds() method has been deprecated and will no longer work in Contao 5.0. Use the Contao\OptInModel::findByRelatedTableAndIds() method instead.', E_USER_DEPRECATED); + $t = static::$strTable; $objDatabase = Database::getInstance(); @@ -119,6 +128,47 @@ public static function findOneByRelatedTableAndId($strTable, $intId, array $arrO return new static($objResult); } + /** + * Find opt-in tokens by their related table and ID + * + * @param string $strTable + * @param array $arrIds + * @param array $arrOptions + * + * @return Collection|OptInModel[]|OptInModel|null + */ + public static function findByRelatedTableAndIds($strTable, array $arrIds, array $arrOptions=array()) + { + $t = static::$strTable; + $objDatabase = Database::getInstance(); + + $objResult = $objDatabase->prepare("SELECT * FROM $t WHERE $t.id IN (SELECT pid FROM tl_opt_in_related WHERE relTable=? AND relId IN(" . implode(',', array_map('\intval', $arrIds)) . ")) ORDER BY $t.createdOn DESC") + ->execute($strTable, $arrIds); + + if ($objResult->numRows < 1) + { + return null; + } + + $arrModels = array(); + $objRegistry = Registry::getInstance(); + + while ($objResult->next()) + { + /** @var OptInModel|Model $objOptIn */ + if ($objOptIn = $objRegistry->fetch($t, $objResult->id)) + { + $arrModels[] = $objOptIn; + } + else + { + $arrModels[] = new static($objResult->row()); + } + } + + return static::createCollection($arrModels, $t); + } + /** * Delete the related records if the model is deleted *
core-bundle/src/Resources/contao/modules/ModulePassword.php+25 −3 modified@@ -177,13 +177,35 @@ protected function setNewPassword() $optIn = System::getContainer()->get('contao.opt-in'); // Find an unconfirmed token with only one related record - if ((!$optInToken = $optIn->find(Input::get('token'))) || $optInToken->isConfirmed() || \count($arrRelated = $optInToken->getRelatedRecords()) != 1 || key($arrRelated) != 'tl_member' || \count($arrIds = current($arrRelated)) != 1 || (!$objMember = MemberModel::findByPk($arrIds[0]))) + if ((!$optInToken = $optIn->find(Input::get('token'))) || !$optInToken->isValid() || \count($arrRelated = $optInToken->getRelatedRecords()) != 1 || key($arrRelated) != 'tl_member' || \count($arrIds = current($arrRelated)) != 1 || (!$objMember = MemberModel::findByPk($arrIds[0]))) { $this->strTemplate = 'mod_message'; $this->Template = new FrontendTemplate($this->strTemplate); $this->Template->type = 'error'; - $this->Template->message = $GLOBALS['TL_LANG']['MSC']['accountError']; + $this->Template->message = $GLOBALS['TL_LANG']['MSC']['invalidToken']; + + return; + } + + if ($optInToken->isConfirmed()) + { + $this->strTemplate = 'mod_message'; + + $this->Template = new FrontendTemplate($this->strTemplate); + $this->Template->type = 'error'; + $this->Template->message = $GLOBALS['TL_LANG']['MSC']['tokenConfirmed']; + + return; + } + + if ($optInToken->getEmail() != $objMember->email) + { + $this->strTemplate = 'mod_message'; + + $this->Template = new FrontendTemplate($this->strTemplate); + $this->Template->type = 'error'; + $this->Template->message = $GLOBALS['TL_LANG']['MSC']['tokenEmailMismatch']; return; } @@ -287,7 +309,7 @@ protected function sendPasswordLink($objMember) { /** @var OptIn $optIn */ $optIn = System::getContainer()->get('contao.opt-in'); - $optInToken = $optIn->create('pw-', $objMember->email, array('tl_member'=>array($objMember->id))); + $optInToken = $optIn->create('pw', $objMember->email, array('tl_member'=>array($objMember->id))); // Prepare the simple token data $arrData = $objMember->row();
core-bundle/src/Resources/contao/modules/ModuleRegistration.php+32 −5 modified@@ -464,7 +464,7 @@ protected function sendActivationMail($arrData) { /** @var OptIn $optIn */ $optIn = System::getContainer()->get('contao.opt-in'); - $optInToken = $optIn->create('reg-', $arrData['email'], array('tl_member'=>array($arrData['id']))); + $optInToken = $optIn->create('reg', $arrData['email'], array('tl_member'=>array($arrData['id']))); // Prepare the simple token data $arrTokenData = $arrData; @@ -521,10 +521,26 @@ protected function activateAcount() $optIn = System::getContainer()->get('contao.opt-in'); // Find an unconfirmed token with only one related record - if ((!$optInToken = $optIn->find(Input::get('token'))) || $optInToken->isConfirmed() || \count($arrRelated = $optInToken->getRelatedRecords()) != 1 || key($arrRelated) != 'tl_member' || \count($arrIds = current($arrRelated)) != 1 || (!$objMember = MemberModel::findByPk($arrIds[0]))) + if ((!$optInToken = $optIn->find(Input::get('token'))) || !$optInToken->isValid() || \count($arrRelated = $optInToken->getRelatedRecords()) != 1 || key($arrRelated) != 'tl_member' || \count($arrIds = current($arrRelated)) != 1 || (!$objMember = MemberModel::findByPk($arrIds[0]))) { $this->Template->type = 'error'; - $this->Template->message = $GLOBALS['TL_LANG']['MSC']['accountError']; + $this->Template->message = $GLOBALS['TL_LANG']['MSC']['invalidToken']; + + return; + } + + if ($optInToken->isConfirmed()) + { + $this->Template->type = 'error'; + $this->Template->message = $GLOBALS['TL_LANG']['MSC']['tokenConfirmed']; + + return; + } + + if ($optInToken->getEmail() != $objMember->email) + { + $this->Template->type = 'error'; + $this->Template->message = $GLOBALS['TL_LANG']['MSC']['tokenEmailMismatch']; return; } @@ -576,9 +592,20 @@ protected function resendActivationMail(MemberModel $objMember) /** @var OptIn $optIn */ $optIn = System::getContainer()->get('contao.opt-in'); + $optInToken = null; + $models = OptInModel::findByRelatedTableAndIds('tl_member', array($objMember->id)); + + foreach ($models as $model) + { + // Look for a valid, unconfirmed token + if (($token = $optIn->find($model->token)) && $token->isValid() && !$token->isConfirmed()) + { + $optInToken = $token; + break; + } + } - /** @var OptInModel $model */ - if ((!$model = OptInModel::findOneByRelatedTableAndId('tl_member', $objMember->id)) || (!$optInToken = $optIn->find($model->token))) + if ($optInToken === null) { return; }
core-bundle/tests/OptIn/OptInTest.php+2 −2 modified@@ -43,7 +43,7 @@ public function testCreatesAToken(): void ->willReturn($model) ; - $token = (new OptIn($framework))->create('reg-', 'foo@bar.com', ['tl_member' => 1]); + $token = (new OptIn($framework))->create('reg', 'foo@bar.com', ['tl_member' => 1]); $this->assertStringMatchesFormat('reg-%x', $token->getIdentifier()); $this->assertTrue($token->isValid()); @@ -62,7 +62,7 @@ public function testDoesNotCreateATokenIfThePrefixIsTooLong(): void $this->expectException('InvalidArgumentException'); $this->expectExceptionMessage('The token prefix must not be longer than 6 characters'); - (new OptIn($framework))->create('registration-', 'foo@bar.com', ['tl_member' => 1]); + (new OptIn($framework))->create('registration', 'foo@bar.com', ['tl_member' => 1]); } public function testFindsAToken(): void
core-bundle/tests/OptIn/OptInTokenTest.php+121 −0 modified@@ -42,6 +42,15 @@ public function testReturnsTheEmailAddress(): void $this->assertSame('foo@bar.com', $token->getEmail()); } + public function testInvalidatesAToken(): void + { + /** @var OptInModel|MockObject $model */ + $model = $this->mockClassWithGetterSetter(OptInModel::class, ['invalidatedThrough' => 'foo']); + $token = $this->getToken($model); + + $this->assertFalse($token->isValid()); + } + public function testConfirmsAToken(): void { $properties = [ @@ -51,13 +60,122 @@ public function testConfirmsAToken(): void /** @var OptInModel|MockObject $model */ $model = $this->mockClassWithGetterSetter(OptInModel::class, $properties); + $model + ->expects($this->once()) + ->method('getRelatedRecords') + ->willReturn([]) + ; $token = $this->getToken($model); $token->confirm(); $this->assertTrue($token->isConfirmed()); } + public function testInvalidatesRelatedTokens(): void + { + $properties = [ + 'token' => 'reg-first', + 'createdOn' => time(), + 'confirmedOn' => 0, + ]; + + /** @var OptInModel|MockObject $related */ + $related = $this->mockClassWithGetterSetter(OptInModel::class, $properties); + $related + ->expects($this->once()) + ->method('save') + ; + + $related + ->expects($this->once()) + ->method('getRelatedRecords') + ->willReturn(['tl_user' => [2]]) + ; + + $adapter = $this->mockAdapter(['findByRelatedTableAndIds']); + $adapter + ->expects($this->once()) + ->method('findByRelatedTableAndIds') + ->with('tl_user', [2]) + ->willReturn([$related]) + ; + + $framework = $this->mockContaoFramework([OptInModel::class => $adapter]); + + $properties = [ + 'token' => 'reg-second', + 'createdOn' => time(), + 'confirmedOn' => 0, + ]; + + /** @var OptInModel|MockObject $model */ + $model = $this->mockClassWithGetterSetter(OptInModel::class, $properties); + $model + ->expects($this->once()) + ->method('getRelatedRecords') + ->willReturn(['tl_user' => [2]]) + ; + + $token = $this->getToken($model, $framework); + $token->confirm(); + + $this->assertTrue($token->isConfirmed()); + $this->assertSame('reg-second', $related->invalidatedThrough); + } + + public function testDoesNotInvalidateRelatedTokensIfTheRelatedRecordsDoNotMatch(): void + { + $properties = [ + 'token' => 'reg-first', + 'createdOn' => time(), + 'confirmedOn' => 0, + ]; + + /** @var OptInModel|MockObject $related */ + $related = $this->mockClassWithGetterSetter(OptInModel::class, $properties); + $related + ->expects($this->never()) + ->method('save') + ; + + $related + ->expects($this->once()) + ->method('getRelatedRecords') + ->willReturn(['tl_user' => [2, 3]]) + ; + + $adapter = $this->mockAdapter(['findByRelatedTableAndIds']); + $adapter + ->expects($this->once()) + ->method('findByRelatedTableAndIds') + ->with('tl_user', [2]) + ->willReturn([$related]) + ; + + $framework = $this->mockContaoFramework([OptInModel::class => $adapter]); + + $properties = [ + 'token' => 'reg-second', + 'createdOn' => time(), + 'confirmedOn' => 0, + ]; + + /** @var OptInModel|MockObject $model */ + $model = $this->mockClassWithGetterSetter(OptInModel::class, $properties); + $model + ->expects($this->once()) + ->method('getRelatedRecords') + ->willReturn(['tl_user' => [2]]) + ; + + $token = $this->getToken($model, $framework); + $token->confirm(); + + $this->assertTrue($token->isConfirmed()); + $this->assertNull($related->invalidatedThrough); + } + public function testDoesNotConfirmAConfirmedToken(): void { $properties = [ @@ -220,6 +338,9 @@ public function testDoesNotRequireSubjectAndTextToResendToken(): void $this->assertTrue($token->hasBeenSent()); } + /** + * @param ContaoFramework|MockObject|null $framework + */ private function getToken(OptInModel $model, ContaoFramework $framework = null): OptInTokenInterface { if (null === $framework) {
newsletter-bundle/src/Resources/contao/modules/ModuleSubscribe.php+35 −8 modified@@ -169,26 +169,53 @@ protected function activateRecipient() $optIn = System::getContainer()->get('contao.opt-in'); // Find an unconfirmed token - if ((!$optInToken = $optIn->find(Input::get('token'))) || $optInToken->isConfirmed() || \count($arrRelated = $optInToken->getRelatedRecords()) < 1 || key($arrRelated) != 'tl_newsletter_recipients' || \count($arrIds = current($arrRelated)) < 1) + if ((!$optInToken = $optIn->find(Input::get('token'))) || !$optInToken->isValid() || \count($arrRelated = $optInToken->getRelatedRecords()) < 1 || key($arrRelated) != 'tl_newsletter_recipients' || \count($arrIds = current($arrRelated)) < 1) { $this->Template->type = 'error'; - $this->Template->message = $GLOBALS['TL_LANG']['MSC']['accountError']; + $this->Template->message = $GLOBALS['TL_LANG']['MSC']['invalidToken']; return; } - $time = time(); - $arrAdd = array(); - $arrCids = array(); + if ($optInToken->isConfirmed()) + { + $this->Template->type = 'error'; + $this->Template->message = $GLOBALS['TL_LANG']['MSC']['tokenConfirmed']; - // Update the subscriptions + return; + } + + $arrRecipients = array(); + + // Validate the token foreach ($arrIds as $intId) { if (!$objRecipient = NewsletterRecipientsModel::findByPk($intId)) { - continue; + $this->Template->type = 'error'; + $this->Template->message = $GLOBALS['TL_LANG']['MSC']['invalidToken']; + + return; } + if ($optInToken->getEmail() != $objRecipient->email) + { + $this->Template->type = 'error'; + $this->Template->message = $GLOBALS['TL_LANG']['MSC']['tokenEmailMismatch']; + + return; + } + + $arrRecipients[] = $objRecipient; + } + + $time = time(); + $arrAdd = array(); + $arrCids = array(); + + // Activate the subscriptions + foreach ($arrRecipients as $objRecipient) + { $arrAdd[] = $objRecipient->id; $arrCids[] = $objRecipient->pid; @@ -333,7 +360,7 @@ protected function addRecipient($strEmail, $arrNew) /** @var OptIn $optIn */ $optIn = System::getContainer()->get('contao.opt-in'); - $optInToken = $optIn->create('nl-', $strEmail, $arrRelated); + $optInToken = $optIn->create('nl', $strEmail, $arrRelated); // Get the channels $objChannel = NewsletterChannelModel::findByIds($arrNew);
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
7- github.com/advisories/GHSA-j99g-qjvx-995gghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2019-10643ghsaADVISORY
- contao.org/en/news.htmlghsax_refsource_CONFIRMWEB
- contao.org/en/news/security-vulnerability-cve-2019-10643.htmlghsax_refsource_CONFIRMWEB
- github.com/FriendsOfPHP/security-advisories/blob/master/contao/contao/CVE-2019-10643.yamlghsaWEB
- github.com/FriendsOfPHP/security-advisories/blob/master/contao/core-bundle/CVE-2019-10643.yamlghsaWEB
- github.com/contao/contao/commit/70348cc812b110831ad66a4f9857883f75649b88ghsaWEB
News mentions
0No linked articles in our index yet.