Skip to content

Commit c3d02d4

Browse files
committed
Harden UsersController actions
1 parent 1f30619 commit c3d02d4

3 files changed

Lines changed: 29 additions & 26 deletions

File tree

CHANGELOG-WIP.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,4 @@
4646
- Fixed [moderate-severity](https://github.com/craftcms/cms/security/policy#severity--remediation) permission escalation vulnerabilities. (GHSA-2xfc-g69j-x2mp, GHSA-jxm3-pmm2-9gf6)
4747
- Fixed a [high-severity](https://github.com/craftcms/cms/security/policy#severity--remediation) SSRF and SSTI vulnerability. (GHSA-5fvc-7894-ghp4)
4848
- Fixed a [moderate-severity](https://github.com/craftcms/cms/security/policy#severity--remediation) SSTI vulnerability. (GHSA-qc86-q28f-ggww)
49+
- Fixed a [moderate-severity](https://github.com/craftcms/cms/security/policy#severity--remediation) user account enumeration vulnerability.

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Release Notes for Craft CMS 4
22

3+
## Unreleased
4+
5+
- Fixed a [moderate-severity](https://github.com/craftcms/cms/security/policy#severity--remediation) user account enumeration vulnerability.
6+
37
## 4.17.0-beta.1 - 2026-01-20
48

59
### Administration

src/controllers/UsersController.php

Lines changed: 24 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use craft\errors\InvalidElementException;
1919
use craft\errors\UploadFailedException;
2020
use craft\errors\UserLockedException;
21+
use craft\errors\WrongEditionException;
2122
use craft\events\DefineUserContentSummaryEvent;
2223
use craft\events\FindLoginUserEvent;
2324
use craft\events\InvalidUserTokenEvent;
@@ -163,7 +164,6 @@ class UsersController extends Controller
163164
'logout' => self::ALLOW_ANONYMOUS_LIVE | self::ALLOW_ANONYMOUS_OFFLINE,
164165
'impersonate-with-token' => self::ALLOW_ANONYMOUS_LIVE | self::ALLOW_ANONYMOUS_OFFLINE,
165166
'save-user' => self::ALLOW_ANONYMOUS_LIVE,
166-
'send-activation-email' => self::ALLOW_ANONYMOUS_LIVE | self::ALLOW_ANONYMOUS_OFFLINE,
167167
'send-password-reset-email' => self::ALLOW_ANONYMOUS_LIVE | self::ALLOW_ANONYMOUS_OFFLINE,
168168
'set-password' => self::ALLOW_ANONYMOUS_LIVE | self::ALLOW_ANONYMOUS_OFFLINE,
169169
'verify-email' => self::ALLOW_ANONYMOUS_LIVE | self::ALLOW_ANONYMOUS_OFFLINE,
@@ -294,7 +294,7 @@ private function _findLoginUser(string $loginName): ?User
294294
*/
295295
public function actionImpersonate(): ?Response
296296
{
297-
$this->requirePostRequest();
297+
$this->userActionChecks();
298298

299299
$userSession = Craft::$app->getUser();
300300
$userId = $this->request->getRequiredBodyParam('userId');
@@ -332,7 +332,7 @@ public function actionImpersonate(): ?Response
332332
*/
333333
public function actionGetImpersonationUrl(): Response
334334
{
335-
$this->requirePostRequest();
335+
$this->userActionChecks();
336336

337337
$userId = $this->request->getBodyParam('userId');
338338
$user = Craft::$app->getUsers()->getUserById($userId);
@@ -607,6 +607,7 @@ public function actionSendPasswordResetEmail(): ?Response
607607
*/
608608
public function actionGetPasswordResetUrl(): Response
609609
{
610+
$this->userActionChecks();
610611
$this->requirePermission('administrateUsers');
611612

612613
if (!$this->_verifyElevatedSession()) {
@@ -777,7 +778,7 @@ public function actionVerifyEmail(): Response
777778
*/
778779
public function actionEnableUser(): ?Response
779780
{
780-
$this->requirePostRequest();
781+
$this->userActionChecks();
781782

782783
$userId = $this->request->getRequiredBodyParam('userId');
783784
$user = Craft::$app->getUsers()->getUserById($userId);
@@ -814,8 +815,8 @@ public function actionEnableUser(): ?Response
814815
*/
815816
public function actionActivateUser(): ?Response
816817
{
818+
$this->userActionChecks();
817819
$this->requirePermission('administrateUsers');
818-
$this->requirePostRequest();
819820
$userVariable = $this->request->getValidatedBodyParam('userVariable') ?? 'user';
820821

821822
$userId = $this->request->getRequiredBodyParam('userId');
@@ -1779,7 +1780,7 @@ public function actionDeleteUserPhoto(): Response
17791780
*/
17801781
public function actionSendActivationEmail(): ?Response
17811782
{
1782-
$this->requirePostRequest();
1783+
$this->userActionChecks();
17831784

17841785
$userId = $this->request->getRequiredBodyParam('userId');
17851786

@@ -1833,7 +1834,7 @@ public function actionSendActivationEmail(): ?Response
18331834
*/
18341835
public function actionUnlockUser(): Response
18351836
{
1836-
$this->requirePostRequest();
1837+
$this->userActionChecks();
18371838
$this->requirePermission('moderateUsers');
18381839

18391840
$userId = $this->request->getRequiredBodyParam('userId');
@@ -1871,7 +1872,7 @@ public function actionUnlockUser(): Response
18711872
*/
18721873
public function actionSuspendUser(): ?Response
18731874
{
1874-
$this->requirePostRequest();
1875+
$this->userActionChecks();
18751876
$this->requirePermission('moderateUsers');
18761877

18771878
$userId = $this->request->getRequiredBodyParam('userId');
@@ -1952,7 +1953,7 @@ public function actionUserContentSummary(): Response
19521953
*/
19531954
public function actionDeactivateUser(): ?Response
19541955
{
1955-
$this->requirePostRequest();
1956+
$this->userActionChecks();
19561957

19571958
$userId = $this->request->getRequiredBodyParam('userId');
19581959
$user = Craft::$app->getUsers()->getUserById($userId);
@@ -2046,7 +2047,7 @@ public function actionDeleteUser(): ?Response
20462047
*/
20472048
public function actionUnsuspendUser(): ?Response
20482049
{
2049-
$this->requirePostRequest();
2050+
$this->userActionChecks();
20502051
$this->requirePermission('moderateUsers');
20512052

20522053
$userId = $this->request->getRequiredBodyParam('userId');
@@ -2214,22 +2215,6 @@ public function actionSaveFieldLayout(): ?Response
22142215
return $this->redirectToPostedUrl();
22152216
}
22162217

2217-
/**
2218-
* Verifies a password for a user.
2219-
*
2220-
* @return Response|null
2221-
*/
2222-
public function actionVerifyPassword(): ?Response
2223-
{
2224-
$this->requireAcceptsJson();
2225-
2226-
if ($this->_verifyExistingPassword()) {
2227-
return $this->asSuccess();
2228-
}
2229-
2230-
return $this->asFailure(Craft::t('app', 'Invalid password.'));
2231-
}
2232-
22332218
/**
22342219
* Handles a failed login attempt.
22352220
*
@@ -2819,4 +2804,17 @@ private function clearPassword(ModelInterface|Model $model): void
28192804
$model->currentPassword = null;
28202805
}
28212806
}
2807+
2808+
/**
2809+
* @throws BadRequestHttpException
2810+
* @throws ForbiddenHttpException
2811+
* @throws WrongEditionException
2812+
*/
2813+
private function userActionChecks(): void
2814+
{
2815+
Craft::$app->requireEdition(Craft::Pro);
2816+
$this->requirePostRequest();
2817+
$this->requireCpRequest();
2818+
$this->requirePermission('editUsers');
2819+
}
28222820
}

0 commit comments

Comments
 (0)