VYPR
High severityNVD Advisory· Published Mar 4, 2026· Updated Mar 4, 2026

Craft has an unauthenticated activation email trigger with potential user enumeration

CVE-2026-29069

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.

PackageAffected versionsPatched versions
craftcms/cmsPackagist
>= 5.0.0-RC1, < 5.9.0-beta.25.9.0-beta.2
craftcms/cmsPackagist
>= 4.0.0-RC1, < 4.17.0-beta.24.17.0-beta.2

Affected products

1

Patches

1
c3d02d4a7246

Harden UsersController actions

https://github.com/craftcms/cmsbrandonkellyJan 21, 2026via ghsa
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

News mentions

0

No linked articles in our index yet.