@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add a rate limit for guessing old passwords when changing passwords

Summary:
Depends on D18904. Ref T13043. If an attacker compromises a victim's session and bypasses their MFA, they can try to guess the user's current account password by making repeated requests to change it: if they guess the right "Old Password", they get a different error than if they don't.

I don't think this is really a very serious concern (the attacker already got a session and MFA, if configured, somehow; many installs don't use passwords anyway) but we get occasional reports about it from HackerOne. Technically, it's better policy to rate limit it, and this should reduce the reports we receive.

Test Plan: Tried to change password over and over again, eventually got rated limited. Used `bin/auth unlimit` to clear the limit, changed password normally without issues.

Reviewers: amckinley

Reviewed By: amckinley

Maniphest Tasks: T13043

Differential Revision: https://secure.phabricator.com/D18906

+45 -5
+2
src/__phutil_library_map__.php
··· 2033 2033 'PhabricatorAuthApplication' => 'applications/auth/application/PhabricatorAuthApplication.php', 2034 2034 'PhabricatorAuthAuthFactorPHIDType' => 'applications/auth/phid/PhabricatorAuthAuthFactorPHIDType.php', 2035 2035 'PhabricatorAuthAuthProviderPHIDType' => 'applications/auth/phid/PhabricatorAuthAuthProviderPHIDType.php', 2036 + 'PhabricatorAuthChangePasswordAction' => 'applications/auth/action/PhabricatorAuthChangePasswordAction.php', 2036 2037 'PhabricatorAuthConduitAPIMethod' => 'applications/auth/conduit/PhabricatorAuthConduitAPIMethod.php', 2037 2038 'PhabricatorAuthConduitTokenRevoker' => 'applications/auth/revoker/PhabricatorAuthConduitTokenRevoker.php', 2038 2039 'PhabricatorAuthConfirmLinkController' => 'applications/auth/controller/PhabricatorAuthConfirmLinkController.php', ··· 7321 7322 'PhabricatorAuthApplication' => 'PhabricatorApplication', 7322 7323 'PhabricatorAuthAuthFactorPHIDType' => 'PhabricatorPHIDType', 7323 7324 'PhabricatorAuthAuthProviderPHIDType' => 'PhabricatorPHIDType', 7325 + 'PhabricatorAuthChangePasswordAction' => 'PhabricatorSystemAction', 7324 7326 'PhabricatorAuthConduitAPIMethod' => 'ConduitAPIMethod', 7325 7327 'PhabricatorAuthConduitTokenRevoker' => 'PhabricatorAuthRevoker', 7326 7328 'PhabricatorAuthConfirmLinkController' => 'PhabricatorAuthController',
+22
src/applications/auth/action/PhabricatorAuthChangePasswordAction.php
··· 1 + <?php 2 + 3 + final class PhabricatorAuthChangePasswordAction 4 + extends PhabricatorSystemAction { 5 + 6 + const TYPECONST = 'auth.password'; 7 + 8 + public function getActionConstant() { 9 + return self::TYPECONST; 10 + } 11 + 12 + public function getScoreThreshold() { 13 + return 20 / phutil_units('1 hour in seconds'); 14 + } 15 + 16 + public function getLimitExplanation() { 17 + return pht( 18 + 'You have failed to enter the correct account password too often in '. 19 + 'a short period of time.'); 20 + } 21 + 22 + }
+21 -5
src/applications/settings/panel/PhabricatorPasswordSettingsPanel.php
··· 25 25 } 26 26 27 27 public function processRequest(AphrontRequest $request) { 28 - $user = $request->getUser(); 28 + $viewer = $request->getUser(); 29 + $user = $this->getUser(); 30 + 29 31 $content_source = PhabricatorContentSource::newFromRequest($request); 30 32 31 33 $token = id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession( 32 - $user, 34 + $viewer, 33 35 $request, 34 36 '/settings/'); 35 37 ··· 44 46 $account_type = PhabricatorAuthPassword::PASSWORD_TYPE_ACCOUNT; 45 47 46 48 $password_objects = id(new PhabricatorAuthPasswordQuery()) 47 - ->setViewer($user) 49 + ->setViewer($viewer) 48 50 ->withObjectPHIDs(array($user->getPHID())) 49 51 ->withPasswordTypes(array($account_type)) 50 52 ->withIsRevoked(false) ··· 63 65 64 66 $errors = array(); 65 67 if ($request->isFormPost()) { 68 + // Rate limit guesses about the old password. This page requires MFA and 69 + // session compromise already, so this is mostly just to stop researchers 70 + // from reporting this as a vulnerability. 71 + PhabricatorSystemActionEngine::willTakeAction( 72 + array($viewer->getPHID()), 73 + new PhabricatorAuthChangePasswordAction(), 74 + 1); 75 + 66 76 $envelope = new PhutilOpaqueEnvelope($request->getStr('old_pw')); 67 77 68 78 $engine = id(new PhabricatorAuthPasswordEngine()) 69 - ->setViewer($user) 79 + ->setViewer($viewer) 70 80 ->setContentSource($content_source) 71 81 ->setPasswordType($account_type) 72 82 ->setObject($user); ··· 79 89 $e_old = pht('Invalid'); 80 90 } else { 81 91 $e_old = null; 92 + 93 + // Refund the user an action credit for getting the password right. 94 + PhabricatorSystemActionEngine::willTakeAction( 95 + array($viewer->getPHID()), 96 + new PhabricatorAuthChangePasswordAction(), 97 + -1); 82 98 } 83 99 84 100 $pass = $request->getStr('new_pw'); ··· 142 158 } 143 159 144 160 $form = id(new AphrontFormView()) 145 - ->setViewer($user) 161 + ->setViewer($viewer) 146 162 ->appendChild( 147 163 id(new AphrontFormPasswordControl()) 148 164 ->setLabel(pht('Old Password'))