From f0262a8fd9ab5fb51b178ace3c3487351217f5a0 Mon Sep 17 00:00:00 2001 From: Daniel Hensby Date: Mon, 4 Sep 2017 17:11:02 +0100 Subject: [PATCH] [SS-2017-005] User enumeration via timing attack mitigated --- security/LoginForm.php | 11 ++++++++++- security/MemberAuthenticator.php | 9 +++++++++ security/MemberLoginForm.php | 14 ++++++++++---- 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/security/LoginForm.php b/security/LoginForm.php index dbc047ec7..331950440 100644 --- a/security/LoginForm.php +++ b/security/LoginForm.php @@ -18,9 +18,18 @@ abstract class LoginForm extends Form { * form. * @var string */ - protected $authenticator_class; + /** + * The minimum amount of time authenticating is allowed to take in milliseconds. + * + * Protects against timing enumeration attacks + * + * @config + * @var int + */ + private static $min_auth_time = 350; + /** * Get the authenticator instance * diff --git a/security/MemberAuthenticator.php b/security/MemberAuthenticator.php index 29005bb35..b0baf11da 100644 --- a/security/MemberAuthenticator.php +++ b/security/MemberAuthenticator.php @@ -150,6 +150,10 @@ class MemberAuthenticator extends Authenticator { * @see Security::setDefaultAdmin() */ public static function authenticate($data, Form $form = null) { + // minimum execution time for authenticating a member + $minExecTime = LoginForm::config()->min_auth_time / 1000; + $startTime = microtime(true); + // Find authenticated member $member = static::authenticate_member($data, $form, $success); @@ -170,6 +174,11 @@ class MemberAuthenticator extends Authenticator { if($success) Session::clear('BackURL'); + $waitFor = $minExecTime - (microtime(true) - $startTime); + if ($waitFor > 0) { + usleep($waitFor * 1000000); + } + return $success ? $member : null; } diff --git a/security/MemberLoginForm.php b/security/MemberLoginForm.php index dfacee77a..a9146f7f9 100644 --- a/security/MemberLoginForm.php +++ b/security/MemberLoginForm.php @@ -294,6 +294,10 @@ JS; * @param array $data Submitted data */ public function forgotPassword($data) { + // minimum execution time for authenticating a member + $minExecTime = self::config()->min_auth_time / 1000; + $startTime = microtime(true); + // Ensure password is given if(empty($data['Email'])) { $this->sessionMessage( @@ -311,10 +315,8 @@ JS; // Allow vetoing forgot password requests $results = $this->extend('forgotPassword', $member); if($results && is_array($results) && in_array(false, $results, true)) { - return $this->controller->redirect('Security/lostpassword'); - } - - if($member) { + $this->controller->redirect('Security/lostpassword'); + } elseif ($member) { $token = $member->generateAutologinTokenAndStoreHash(); $e = Member_ForgotPasswordEmail::create(); @@ -338,6 +340,10 @@ JS; $this->controller->redirect('Security/lostpassword'); } + $waitFor = $minExecTime - (microtime(true) - $startTime); + if ($waitFor > 0) { + usleep($waitFor * 1000000); + } } }