Skip to content

Commit 3e2bcaa

Browse files
author
Damian Mooyman
authored
Merge pull request #54 from silverstripe-security/pulls/3.5/ss-2017-009
[ss-2017-009] Prevent disclosure of sensitive information via LoginAttempt (3.5 branch)
2 parents 975d462 + 6ba00e8 commit 3e2bcaa

File tree

4 files changed

+51
-38
lines changed

4 files changed

+51
-38
lines changed

security/LoginAttempt.php

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,20 @@
1212
* @package framework
1313
* @subpackage security
1414
*
15-
* @property string Email Email address used for login attempt
16-
* @property string Status Status of the login attempt, either 'Success' or 'Failure'
17-
* @property string IP IP address of user attempting to login
15+
* @property string $Email Email address used for login attempt. @deprecated 3.0...5.0
16+
* @property string $EmailHashed sha1 hashed Email address used for login attempt
17+
* @property string $Status Status of the login attempt, either 'Success' or 'Failure'
18+
* @property string $IP IP address of user attempting to login
1819
*
19-
* @property int MemberID ID of the Member, only if Member with Email exists
20+
* @property int $MemberID ID of the Member, only if Member with Email exists
2021
*
2122
* @method Member Member() Member object of the user trying to log in, only if Member with Email exists
2223
*/
2324
class LoginAttempt extends DataObject {
2425

2526
private static $db = array(
26-
'Email' => 'Varchar(255)',
27+
'Email' => 'Varchar(255)', // Remove in 5.0
28+
'EmailHashed' => 'Varchar(255)',
2729
'Status' => "Enum('Success,Failure')",
2830
'IP' => 'Varchar(255)',
2931
);
@@ -32,24 +34,38 @@ class LoginAttempt extends DataObject {
3234
'Member' => 'Member', // only linked if the member actually exists
3335
);
3436

35-
private static $has_many = array();
36-
37-
private static $many_many = array();
38-
39-
private static $belongs_many_many = array();
40-
41-
/**
42-
*
43-
* @param boolean $includerelations a boolean value to indicate if the labels returned include relation fields
44-
*
45-
*/
4637
public function fieldLabels($includerelations = true) {
4738
$labels = parent::fieldLabels($includerelations);
4839
$labels['Email'] = _t('LoginAttempt.Email', 'Email Address');
40+
$labels['EmailHashed'] = _t('LoginAttempt.EmailHashed', 'Email Address (hashed)');
4941
$labels['Status'] = _t('LoginAttempt.Status', 'Status');
5042
$labels['IP'] = _t('LoginAttempt.IP', 'IP Address');
5143

5244
return $labels;
5345
}
5446

47+
/**
48+
* Set email used for this attempt
49+
*
50+
* @param string $email
51+
* @return $this
52+
*/
53+
public function setEmail($email) {
54+
// Store hashed email only
55+
$this->EmailHashed = sha1($email);
56+
return $this;
57+
}
58+
59+
/**
60+
* Get all login attempts for the given email address
61+
*
62+
* @param string $email
63+
* @return DataList
64+
*/
65+
public static function getByEmail($email) {
66+
return static::get()->filterAny(array(
67+
'Email' => $email,
68+
'EmailHashed' => sha1($email),
69+
));
70+
}
5571
}

security/Member.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -407,9 +407,10 @@ public function isLockedOut() {
407407
return false;
408408
}
409409

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

414415
if ($attempts->count() < $this->config()->lock_out_after_incorrect_logins) {
415416
return false;

tests/security/MemberAuthenticatorTest.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,8 @@ public function testNonExistantMemberGetsLoginAttemptRecorded()
196196
$this->assertNull($response);
197197
$this->assertCount(1, LoginAttempt::get());
198198
$attempt = LoginAttempt::get()->first();
199-
$this->assertEquals($email, $attempt->Email);
199+
$this->assertEmpty($attempt->Email); // Doesn't store potentially sensitive data
200+
$this->assertEquals(sha1($email), $attempt->EmailHashed);
200201
$this->assertEquals('Failure', $attempt->Status);
201202

202203
}

tests/security/SecurityTest.php

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -507,25 +507,21 @@ public function testUnsuccessfulLoginAttempts() {
507507

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

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

537533
/* SUCCESSFUL ATTEMPTS ARE LOGGED */
538534
$this->doTestLoginForm('testuser@example.com', '1nitialPassword');
539-
$attempt = DataObject::get_one('LoginAttempt', array(
540-
'"LoginAttempt"."Email"' => 'testuser@example.com'
541-
));
535+
$attempt = LoginAttempt::getByEmail('testuser@example.com')->first();
542536
$member = DataObject::get_one('Member', array(
543537
'"Member"."Email"' => 'testuser@example.com'
544538
));
545-
$this->assertTrue(is_object($attempt));
539+
$this->assertInstanceOf('LoginAttempt', $attempt);
546540
$this->assertEquals($attempt->Status, 'Success');
547-
$this->assertEquals($attempt->Email, 'testuser@example.com');
541+
$this->assertEmpty($attempt->Email); // Doesn't store potentially sensitive data
542+
$this->assertEquals($attempt->EmailHashed, sha1('testuser@example.com'));
548543
$this->assertEquals($attempt->Member(), $member);
549544
}
550545

0 commit comments

Comments
 (0)