Craft has an unauthenticated activation email trigger with potential user enumeration
Description
Craft is a content management system (CMS). Prior to 5.9.0-beta.2 and 4.17.0-beta.2, the actionSendActivationEmail() endpoint is accessible to unauthenticated users and does not require a permission check for pending users. An attacker with no prior access can trigger activation emails for any pending user account by knowing or guessing the user ID. If the attacker controls the target user’s email address, they can activate the account and gain access to the system. This vulnerability is fixed in 5.9.0-beta.2 and 4.17.0-beta.2.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Craft CMS before 5.9.0-beta.2 and 4.17.0-beta.2 allows unauthenticated attackers to trigger activation emails for any pending user by ID, enabling account takeover if the attacker controls the user's email.
Vulnerability
The actionSendActivationEmail() endpoint in Craft CMS is accessible to unauthenticated users and accepts a userId parameter without verifying ownership. The endpoint is intentionally listed in the allowAnonymous array to support legitimate self-service resending of activation emails, but it lacks a permission check for pending users [1][3]. This allows any unauthenticated visitor to trigger activation emails for any pending user account by knowing or guessing the user's numeric ID [1][2].
Exploitation
An attacker with no prior access can enumerate user IDs and send activation emails to the corresponding emails to the target pending user's registered email address. The attacker does not need to be logged in or have any permissions. If the attacker also controls the target's email (e.g., through compromised email, through compromised credentials, typosquatting,, or a shared mailbox), they can receive the activation link and activate the account [1][3]. The fix removes send-activation-email from the anonymous access list in UsersController [2].
Impact and
Mitigation
Successful exploitation can lead to unauthorized access to the Craft CMS control panel with the privileges of the activated user. The vulnerability also introduces a user enumeration vector, as an attacker can distinguish between valid and invalid user IDs based on whether an activation email is sent. The issue [1][3]. Administrators should update Craft CMS to version 5.9.0-beta.2 or 4.17.0-beta.2, where the endpoint is properly protected [1][2][3].
AI Insight generated on May 18, 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 |
|---|---|---|
craftcms/cmsPackagist | >= 5.0.0-RC1, < 5.9.0-beta.2 | 5.9.0-beta.2 |
craftcms/cmsPackagist | >= 4.0.0-RC1, < 4.17.0-beta.2 | 4.17.0-beta.2 |
Affected products
1Patches
1c3d02d4a7246Harden UsersController actions
3 files changed · +29 −26
CHANGELOG.md+4 −0 modified@@ -1,5 +1,9 @@ # Release Notes for Craft CMS 4 +## Unreleased + +- Fixed a [moderate-severity](https://github.com/craftcms/cms/security/policy#severity--remediation) user account enumeration vulnerability. + ## 4.17.0-beta.1 - 2026-01-20 ### Administration
CHANGELOG-WIP.md+1 −0 modified@@ -46,3 +46,4 @@ - Fixed [moderate-severity](https://github.com/craftcms/cms/security/policy#severity--remediation) permission escalation vulnerabilities. (GHSA-2xfc-g69j-x2mp, GHSA-jxm3-pmm2-9gf6) - Fixed a [high-severity](https://github.com/craftcms/cms/security/policy#severity--remediation) SSRF and SSTI vulnerability. (GHSA-5fvc-7894-ghp4) - Fixed a [moderate-severity](https://github.com/craftcms/cms/security/policy#severity--remediation) SSTI vulnerability. (GHSA-qc86-q28f-ggww) +- Fixed a [moderate-severity](https://github.com/craftcms/cms/security/policy#severity--remediation) user account enumeration vulnerability.
src/controllers/UsersController.php+24 −26 modified@@ -18,6 +18,7 @@ use craft\errors\InvalidElementException; use craft\errors\UploadFailedException; use craft\errors\UserLockedException; +use craft\errors\WrongEditionException; use craft\events\DefineUserContentSummaryEvent; use craft\events\FindLoginUserEvent; use craft\events\InvalidUserTokenEvent; @@ -163,7 +164,6 @@ class UsersController extends Controller 'logout' => self::ALLOW_ANONYMOUS_LIVE | self::ALLOW_ANONYMOUS_OFFLINE, 'impersonate-with-token' => self::ALLOW_ANONYMOUS_LIVE | self::ALLOW_ANONYMOUS_OFFLINE, 'save-user' => self::ALLOW_ANONYMOUS_LIVE, - 'send-activation-email' => self::ALLOW_ANONYMOUS_LIVE | self::ALLOW_ANONYMOUS_OFFLINE, 'send-password-reset-email' => self::ALLOW_ANONYMOUS_LIVE | self::ALLOW_ANONYMOUS_OFFLINE, 'set-password' => self::ALLOW_ANONYMOUS_LIVE | self::ALLOW_ANONYMOUS_OFFLINE, 'verify-email' => self::ALLOW_ANONYMOUS_LIVE | self::ALLOW_ANONYMOUS_OFFLINE, @@ -294,7 +294,7 @@ private function _findLoginUser(string $loginName): ?User */ public function actionImpersonate(): ?Response { - $this->requirePostRequest(); + $this->userActionChecks(); $userSession = Craft::$app->getUser(); $userId = $this->request->getRequiredBodyParam('userId'); @@ -332,7 +332,7 @@ public function actionImpersonate(): ?Response */ public function actionGetImpersonationUrl(): Response { - $this->requirePostRequest(); + $this->userActionChecks(); $userId = $this->request->getBodyParam('userId'); $user = Craft::$app->getUsers()->getUserById($userId); @@ -607,6 +607,7 @@ public function actionSendPasswordResetEmail(): ?Response */ public function actionGetPasswordResetUrl(): Response { + $this->userActionChecks(); $this->requirePermission('administrateUsers'); if (!$this->_verifyElevatedSession()) { @@ -777,7 +778,7 @@ public function actionVerifyEmail(): Response */ public function actionEnableUser(): ?Response { - $this->requirePostRequest(); + $this->userActionChecks(); $userId = $this->request->getRequiredBodyParam('userId'); $user = Craft::$app->getUsers()->getUserById($userId); @@ -814,8 +815,8 @@ public function actionEnableUser(): ?Response */ public function actionActivateUser(): ?Response { + $this->userActionChecks(); $this->requirePermission('administrateUsers'); - $this->requirePostRequest(); $userVariable = $this->request->getValidatedBodyParam('userVariable') ?? 'user'; $userId = $this->request->getRequiredBodyParam('userId'); @@ -1779,7 +1780,7 @@ public function actionDeleteUserPhoto(): Response */ public function actionSendActivationEmail(): ?Response { - $this->requirePostRequest(); + $this->userActionChecks(); $userId = $this->request->getRequiredBodyParam('userId'); @@ -1833,7 +1834,7 @@ public function actionSendActivationEmail(): ?Response */ public function actionUnlockUser(): Response { - $this->requirePostRequest(); + $this->userActionChecks(); $this->requirePermission('moderateUsers'); $userId = $this->request->getRequiredBodyParam('userId'); @@ -1871,7 +1872,7 @@ public function actionUnlockUser(): Response */ public function actionSuspendUser(): ?Response { - $this->requirePostRequest(); + $this->userActionChecks(); $this->requirePermission('moderateUsers'); $userId = $this->request->getRequiredBodyParam('userId'); @@ -1952,7 +1953,7 @@ public function actionUserContentSummary(): Response */ public function actionDeactivateUser(): ?Response { - $this->requirePostRequest(); + $this->userActionChecks(); $userId = $this->request->getRequiredBodyParam('userId'); $user = Craft::$app->getUsers()->getUserById($userId); @@ -2046,7 +2047,7 @@ public function actionDeleteUser(): ?Response */ public function actionUnsuspendUser(): ?Response { - $this->requirePostRequest(); + $this->userActionChecks(); $this->requirePermission('moderateUsers'); $userId = $this->request->getRequiredBodyParam('userId'); @@ -2214,22 +2215,6 @@ public function actionSaveFieldLayout(): ?Response return $this->redirectToPostedUrl(); } - /** - * Verifies a password for a user. - * - * @return Response|null - */ - public function actionVerifyPassword(): ?Response - { - $this->requireAcceptsJson(); - - if ($this->_verifyExistingPassword()) { - return $this->asSuccess(); - } - - return $this->asFailure(Craft::t('app', 'Invalid password.')); - } - /** * Handles a failed login attempt. * @@ -2819,4 +2804,17 @@ private function clearPassword(ModelInterface|Model $model): void $model->currentPassword = null; } } + + /** + * @throws BadRequestHttpException + * @throws ForbiddenHttpException + * @throws WrongEditionException + */ + private function userActionChecks(): void + { + Craft::$app->requireEdition(Craft::Pro); + $this->requirePostRequest(); + $this->requireCpRequest(); + $this->requirePermission('editUsers'); + } }
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
4- github.com/advisories/GHSA-234q-vvw3-mrfqghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-29069ghsaADVISORY
- github.com/craftcms/cms/commit/c3d02d4a7246f516933f42106c0a67ce062f68d8ghsax_refsource_MISCWEB
- github.com/craftcms/cms/security/advisories/GHSA-234q-vvw3-mrfqghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.