Skip to content

Commit

Permalink
[ss-2017-009] Prevent disclosure of sensitive information via LoginAt…
Browse files Browse the repository at this point in the history
…tempt
  • Loading branch information
Damian Mooyman committed Nov 30, 2017
1 parent b31b22a commit 6ba00e8
Show file tree
Hide file tree
Showing 4 changed files with 51 additions and 38 deletions.
48 changes: 32 additions & 16 deletions security/LoginAttempt.php
Expand Up @@ -12,18 +12,20 @@
* @package framework
* @subpackage security
*
* @property string Email Email address used for login attempt
* @property string Status Status of the login attempt, either 'Success' or 'Failure'
* @property string IP IP address of user attempting to login
* @property string $Email Email address used for login attempt. @deprecated 3.0...5.0
* @property string $EmailHashed sha1 hashed Email address used for login attempt
* @property string $Status Status of the login attempt, either 'Success' or 'Failure'
* @property string $IP IP address of user attempting to login
*
* @property int MemberID ID of the Member, only if Member with Email exists
* @property int $MemberID ID of the Member, only if Member with Email exists
*
* @method Member Member() Member object of the user trying to log in, only if Member with Email exists
*/
class LoginAttempt extends DataObject {

private static $db = array(
'Email' => 'Varchar(255)',
'Email' => 'Varchar(255)', // Remove in 5.0
'EmailHashed' => 'Varchar(255)',
'Status' => "Enum('Success,Failure')",
'IP' => 'Varchar(255)',
);
Expand All @@ -32,24 +34,38 @@ class LoginAttempt extends DataObject {
'Member' => 'Member', // only linked if the member actually exists
);

private static $has_many = array();

private static $many_many = array();

private static $belongs_many_many = array();

/**
*
* @param boolean $includerelations a boolean value to indicate if the labels returned include relation fields
*
*/
public function fieldLabels($includerelations = true) {
$labels = parent::fieldLabels($includerelations);
$labels['Email'] = _t('LoginAttempt.Email', 'Email Address');
$labels['EmailHashed'] = _t('LoginAttempt.EmailHashed', 'Email Address (hashed)');
$labels['Status'] = _t('LoginAttempt.Status', 'Status');
$labels['IP'] = _t('LoginAttempt.IP', 'IP Address');

return $labels;
}

/**
* Set email used for this attempt
*
* @param string $email
* @return $this
*/
public function setEmail($email) {
// Store hashed email only
$this->EmailHashed = sha1($email);
return $this;
}

/**
* Get all login attempts for the given email address
*
* @param string $email
* @return DataList
*/
public static function getByEmail($email) {
return static::get()->filterAny(array(
'Email' => $email,
'EmailHashed' => sha1($email),
));
}
}
7 changes: 4 additions & 3 deletions security/Member.php
Expand Up @@ -407,9 +407,10 @@ public function isLockedOut() {
return false;
}

$attempts = LoginAttempt::get()->filter($filter = array(
'Email' => $this->{static::config()->unique_identifier_field},
))->sort('Created', 'DESC')->limit($this->config()->lock_out_after_incorrect_logins);
$email = $this->{static::config()->unique_identifier_field};
$attempts = LoginAttempt::getByEmail($email)
->sort('Created', 'DESC')
->limit($this->config()->lock_out_after_incorrect_logins);

if ($attempts->count() < $this->config()->lock_out_after_incorrect_logins) {
return false;
Expand Down
3 changes: 2 additions & 1 deletion tests/security/MemberAuthenticatorTest.php
Expand Up @@ -196,7 +196,8 @@ public function testNonExistantMemberGetsLoginAttemptRecorded()
$this->assertNull($response);
$this->assertCount(1, LoginAttempt::get());
$attempt = LoginAttempt::get()->first();
$this->assertEquals($email, $attempt->Email);
$this->assertEmpty($attempt->Email); // Doesn't store potentially sensitive data
$this->assertEquals(sha1($email), $attempt->EmailHashed);
$this->assertEquals('Failure', $attempt->Status);

}
Expand Down
31 changes: 13 additions & 18 deletions tests/security/SecurityTest.php
Expand Up @@ -507,25 +507,21 @@ public function testUnsuccessfulLoginAttempts() {

/* UNSUCCESSFUL ATTEMPTS WITH WRONG PASSWORD FOR EXISTING USER ARE LOGGED */
$this->doTestLoginForm('testuser@example.com', 'wrongpassword');
$attempt = DataObject::get_one('LoginAttempt', array(
'"LoginAttempt"."Email"' => 'testuser@example.com'
));
$this->assertTrue(is_object($attempt));
$member = DataObject::get_one('Member', array(
'"Member"."Email"' => 'testuser@example.com'
));
$attempt = LoginAttempt::getByEmail('testuser@example.com')->first();
$this->assertInstanceOf('LoginAttempt', $attempt);
$member = Member::get()->filter('Email', 'testuser@example.com')->first();
$this->assertEquals($attempt->Status, 'Failure');
$this->assertEquals($attempt->Email, 'testuser@example.com');
$this->assertEmpty($attempt->Email); // Doesn't store potentially sensitive data
$this->assertEquals($attempt->EmailHashed, sha1('testuser@example.com'));
$this->assertEquals($attempt->Member(), $member);

/* UNSUCCESSFUL ATTEMPTS WITH NONEXISTING USER ARE LOGGED */
$this->doTestLoginForm('wronguser@silverstripe.com', 'wrongpassword');
$attempt = DataObject::get_one('LoginAttempt', array(
'"LoginAttempt"."Email"' => 'wronguser@silverstripe.com'
));
$this->assertTrue(is_object($attempt));
$attempt = LoginAttempt::getByEmail('wronguser@silverstripe.com')->first();
$this->assertInstanceOf('LoginAttempt', $attempt);
$this->assertEquals($attempt->Status, 'Failure');
$this->assertEquals($attempt->Email, 'wronguser@silverstripe.com');
$this->assertEmpty($attempt->Email); // Doesn't store potentially sensitive data
$this->assertEquals($attempt->EmailHashed, sha1('wronguser@silverstripe.com'));
$this->assertNotNull(
$this->loginErrorMessage(), 'An invalid email returns a message.'
);
Expand All @@ -536,15 +532,14 @@ public function testSuccessfulLoginAttempts() {

/* SUCCESSFUL ATTEMPTS ARE LOGGED */
$this->doTestLoginForm('testuser@example.com', '1nitialPassword');
$attempt = DataObject::get_one('LoginAttempt', array(
'"LoginAttempt"."Email"' => 'testuser@example.com'
));
$attempt = LoginAttempt::getByEmail('testuser@example.com')->first();
$member = DataObject::get_one('Member', array(
'"Member"."Email"' => 'testuser@example.com'
));
$this->assertTrue(is_object($attempt));
$this->assertInstanceOf('LoginAttempt', $attempt);
$this->assertEquals($attempt->Status, 'Success');
$this->assertEquals($attempt->Email, 'testuser@example.com');
$this->assertEmpty($attempt->Email); // Doesn't store potentially sensitive data
$this->assertEquals($attempt->EmailHashed, sha1('testuser@example.com'));
$this->assertEquals($attempt->Member(), $member);
}

Expand Down

0 comments on commit 6ba00e8

Please sign in to comment.