@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.

Rate limit multi-factor actions

Summary: Ref T4398. Prevent users from brute forcing multi-factor auth by rate limiting attempts. This slightly refines the rate limiting to allow callers to check for a rate limit without adding points, and gives users credit for successfully completing an auth workflow.

Test Plan: Tried to enter hisec with bad credentials 11 times in a row, got rate limited.

Reviewers: btrahan

Reviewed By: btrahan

Subscribers: epriestley

Maniphest Tasks: T4398

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

+104 -5
+8
resources/celerity/map.php
··· 356 356 'rsrc/js/application/conpherence/behavior-pontificate.js' => '53f6f2dd', 357 357 'rsrc/js/application/conpherence/behavior-widget-pane.js' => 'd8ef8659', 358 358 'rsrc/js/application/countdown/timer.js' => '889c96f3', 359 + 'rsrc/js/application/dashboard/behavior-dashboard-async-panel.js' => '4398eabb', 359 360 'rsrc/js/application/differential/DifferentialInlineCommentEditor.js' => 'f2441746', 360 361 'rsrc/js/application/differential/behavior-add-reviewers-and-ccs.js' => '533a187b', 361 362 'rsrc/js/application/differential/behavior-comment-jump.js' => '71755c79', ··· 546 547 'javelin-behavior-conpherence-widget-pane' => 'd8ef8659', 547 548 'javelin-behavior-countdown-timer' => '889c96f3', 548 549 'javelin-behavior-dark-console' => 'e9fdb5e5', 550 + 'javelin-behavior-dashboard-async-panel' => '4398eabb', 549 551 'javelin-behavior-device' => '03d6ed07', 550 552 'javelin-behavior-differential-add-reviewers-and-ccs' => '533a187b', 551 553 'javelin-behavior-differential-comment-jump' => '71755c79', ··· 1072 1074 0 => 'javelin-behavior', 1073 1075 1 => 'javelin-dom', 1074 1076 2 => 'phortune-credit-card-form', 1077 + ), 1078 + '4398eabb' => 1079 + array( 1080 + 0 => 'javelin-behavior', 1081 + 1 => 'javelin-dom', 1082 + 2 => 'javelin-workflow', 1075 1083 ), 1076 1084 '441f2137' => 1077 1085 array(
+2
src/__phutil_library_map__.php
··· 1257 1257 'PhabricatorAuthSessionQuery' => 'applications/auth/query/PhabricatorAuthSessionQuery.php', 1258 1258 'PhabricatorAuthStartController' => 'applications/auth/controller/PhabricatorAuthStartController.php', 1259 1259 'PhabricatorAuthTerminateSessionController' => 'applications/auth/controller/PhabricatorAuthTerminateSessionController.php', 1260 + 'PhabricatorAuthTryFactorAction' => 'applications/auth/action/PhabricatorAuthTryFactorAction.php', 1260 1261 'PhabricatorAuthUnlinkController' => 'applications/auth/controller/PhabricatorAuthUnlinkController.php', 1261 1262 'PhabricatorAuthValidateController' => 'applications/auth/controller/PhabricatorAuthValidateController.php', 1262 1263 'PhabricatorAuthenticationConfigOptions' => 'applications/config/option/PhabricatorAuthenticationConfigOptions.php', ··· 4029 4030 'PhabricatorAuthSessionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 4030 4031 'PhabricatorAuthStartController' => 'PhabricatorAuthController', 4031 4032 'PhabricatorAuthTerminateSessionController' => 'PhabricatorAuthController', 4033 + 'PhabricatorAuthTryFactorAction' => 'PhabricatorSystemAction', 4032 4034 'PhabricatorAuthUnlinkController' => 'PhabricatorAuthController', 4033 4035 'PhabricatorAuthValidateController' => 'PhabricatorAuthController', 4034 4036 'PhabricatorAuthenticationConfigOptions' => 'PhabricatorApplicationConfigOptions',
+21
src/applications/auth/action/PhabricatorAuthTryFactorAction.php
··· 1 + <?php 2 + 3 + final class PhabricatorAuthTryFactorAction extends PhabricatorSystemAction { 4 + 5 + const TYPECONST = 'auth.factor'; 6 + 7 + public function getActionConstant() { 8 + return self::TYPECONST; 9 + } 10 + 11 + public function getScoreThreshold() { 12 + return 10 / phutil_units('1 hour in seconds'); 13 + } 14 + 15 + public function getLimitExplanation() { 16 + return pht( 17 + 'You have failed to verify multi-factor authentication too often in '. 18 + 'a short period of time.'); 19 + } 20 + 21 + }
+22
src/applications/auth/engine/PhabricatorAuthSessionEngine.php
··· 247 247 return $this->issueHighSecurityToken($session, true); 248 248 } 249 249 250 + // Check for a rate limit without awarding points, so the user doesn't 251 + // get partway through the workflow only to get blocked. 252 + PhabricatorSystemActionEngine::willTakeAction( 253 + array($viewer->getPHID()), 254 + new PhabricatorAuthTryFactorAction(), 255 + 0); 256 + 250 257 $validation_results = array(); 251 258 if ($request->isHTTPPost()) { 252 259 $request->validateCSRF(); 253 260 if ($request->getExists(AphrontRequest::TYPE_HISEC)) { 254 261 262 + // Limit factor verification rates to prevent brute force attacks. 263 + PhabricatorSystemActionEngine::willTakeAction( 264 + array($viewer->getPHID()), 265 + new PhabricatorAuthTryFactorAction(), 266 + 1); 267 + 255 268 $ok = true; 256 269 foreach ($factors as $factor) { 257 270 $id = $factor->getID(); ··· 268 281 } 269 282 270 283 if ($ok) { 284 + // Give the user a credit back for a successful factor verification. 285 + PhabricatorSystemActionEngine::willTakeAction( 286 + array($viewer->getPHID()), 287 + new PhabricatorAuthTryFactorAction(), 288 + -1); 289 + 271 290 $until = time() + phutil_units('15 minutes in seconds'); 272 291 $session->setHighSecurityUntil($until); 273 292 ··· 284 303 PhabricatorUserLog::ACTION_ENTER_HISEC); 285 304 $log->save(); 286 305 } else { 306 + 307 + 308 + 287 309 $log = PhabricatorUserLog::initializeNewLog( 288 310 $viewer, 289 311 $viewer->getPHID(),
+51 -5
src/applications/system/engine/PhabricatorSystemActionEngine.php
··· 2 2 3 3 final class PhabricatorSystemActionEngine extends Phobject { 4 4 5 + /** 6 + * Prepare to take an action, throwing an exception if the user has exceeded 7 + * the rate limit. 8 + * 9 + * The `$actors` are a list of strings. Normally this will be a list of 10 + * user PHIDs, but some systems use other identifiers (like email 11 + * addresses). Each actor's score threshold is tracked independently. If 12 + * any actor exceeds the rate limit for the action, this method throws. 13 + * 14 + * The `$action` defines the actual thing being rate limited, and sets the 15 + * limit. 16 + * 17 + * You can pass either a positive, zero, or negative `$score` to this method: 18 + * 19 + * - If the score is positive, the user is given that many points toward 20 + * the rate limit after the limit is checked. Over time, this will cause 21 + * them to hit the rate limit and be prevented from taking further 22 + * actions. 23 + * - If the score is zero, the rate limit is checked but no score changes 24 + * are made. This allows you to check for a rate limit before beginning 25 + * a workflow, so the user doesn't fill in a form only to get rate limited 26 + * at the end. 27 + * - If the score is negative, the user is credited points, allowing them 28 + * to take more actions than the limit normally permits. By awarding 29 + * points for failed actions and credits for successful actions, a 30 + * system can be sensitive to failure without overly restricting 31 + * legitimate uses. 32 + * 33 + * If any actor is exceeding their rate limit, this method throws a 34 + * @{class:PhabricatorSystemActionRateLimitException}. 35 + * 36 + * @param list<string> List of actors. 37 + * @param PhabricatorSystemAction Action being taken. 38 + * @param float Score or credit, see above. 39 + * @return void 40 + */ 5 41 public static function willTakeAction( 6 42 array $actors, 7 43 PhabricatorSystemAction $action, ··· 20 56 } 21 57 } 22 58 23 - $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); 24 - self::recordAction($actors, $action, $score); 25 - unset($unguarded); 59 + if ($score != 0) { 60 + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); 61 + self::recordAction($actors, $action, $score); 62 + unset($unguarded); 63 + } 26 64 } 27 65 28 66 public static function loadBlockedActors( ··· 35 73 36 74 $blocked = array(); 37 75 foreach ($scores as $actor => $actor_score) { 38 - $actor_score = $actor_score + ($score / $window); 76 + // For the purposes of checking for a block, we just use the raw 77 + // persistent score and do not include the score for this action. This 78 + // allows callers to test for a block without adding any points and get 79 + // the same result they would if they were adding points: we only 80 + // trigger a rate limit when the persistent score exceeds the threshold. 39 81 if ($action->shouldBlockActor($actor, $actor_score)) { 40 - $blocked[$actor] = $actor_score; 82 + // When reporting the results, we do include the points for this 83 + // action. This makes the error messages more clear, since they 84 + // more accurately report the number of actions the user has really 85 + // tried to take. 86 + $blocked[$actor] = $actor_score + ($score / $window); 41 87 } 42 88 } 43 89