[IMPROVED] Lockout user on invalid pin/password

This will lock a user account if a password or PIN has been entered
wrong for multiple times in a row. When unlocking the account via admin
panel, both counters are reset so the user can log in again.

This should fix issues with brute force attacks to access user accounts.

Please see configuration dist file for new config options.
Please import SQL upgrade 007 to add new column to user accounts table.

Addresses #670 and should be merged once tested.
This commit is contained in:
Sebastian Grewe 2013-12-07 22:14:14 +01:00
parent 8fbad03457
commit 256b5f59c6
6 changed files with 55 additions and 9 deletions

View File

@ -46,6 +46,7 @@ require_once(CLASS_DIR . '/statscache.class.php');
require_once(CLASS_DIR . '/bitcoin.class.php');
require_once(CLASS_DIR . '/bitcoinwrapper.class.php');
require_once(CLASS_DIR . '/monitoring.class.php');
require_once(CLASS_DIR . '/notification.class.php');
require_once(CLASS_DIR . '/user.class.php');
require_once(CLASS_DIR . '/invitation.class.php');
require_once(CLASS_DIR . '/share.class.php');
@ -53,7 +54,6 @@ require_once(CLASS_DIR . '/worker.class.php');
require_once(CLASS_DIR . '/statistics.class.php');
require_once(CLASS_DIR . '/transaction.class.php');
require_once(CLASS_DIR . '/roundstats.class.php');
require_once(CLASS_DIR . '/notification.class.php');
require_once(CLASS_DIR . '/news.class.php');
require_once(CLASS_DIR . '/api.class.php');
require_once(INCLUDE_DIR . '/lib/Michelf/Markdown.php');

View File

@ -46,6 +46,9 @@ class User extends Base {
public function getUserFailed($id) {
return $this->getSingle($id, 'failed_logins', 'id');
}
public function getUserPinFailed($id) {
return $this->getSingle($id, 'failed_pins', 'id');
}
public function isNoFee($id) {
return $this->getUserNoFee($id);
}
@ -71,10 +74,18 @@ class User extends Base {
$field = array( 'name' => 'failed_logins', 'type' => 'i', 'value' => $value);
return $this->updateSingle($id, $field);
}
public function setUserPinFailed($id, $value) {
$field = array( 'name' => 'failed_pins', 'type' => 'i', 'value' => $value);
return $this->updateSingle($id, $field);
}
private function incUserFailed($id) {
$field = array( 'name' => 'failed_logins', 'type' => 'i', 'value' => $this->getUserFailed($id) + 1);
return $this->updateSingle($id, $field);
}
private function incUserPinFailed($id) {
$field = array( 'name' => 'failed_pins', 'type' => 'i', 'value' => $this->getUserPinFailed($id) + 1);
return $this->updateSingle($id, $field);
}
private function setUserIp($id, $ip) {
$field = array( 'name' => 'loggedIp', 'type' => 's', 'value' => $ip );
return $this->updateSingle($id, $field);
@ -122,8 +133,12 @@ class User extends Base {
return true;
}
$this->setErrorMessage("Invalid username or password");
if ($id = $this->getUserId($username))
if ($id = $this->getUserId($username)) {
$this->incUserFailed($id);
// Check if this account should be locked
if (isset($this->config['maxfailed']['login']) && $this->getUserFailed($id) >= $this->config['maxfailed']['login'])
$this->changeLocked($id);
}
return false;
}
@ -139,12 +154,17 @@ class User extends Base {
$this->debug->append("Confirming PIN for $userId and pin $pin", 2);
$stmt = $this->mysqli->prepare("SELECT pin FROM $this->table WHERE id=? AND pin=? LIMIT 1");
$pin_hash = $this->getHash($pin);
$stmt->bind_param('is', $userId, $pin_hash);
$stmt->execute();
$stmt->bind_result($row_pin);
$stmt->fetch();
$stmt->close();
return $pin_hash === $row_pin;
if ($stmt->bind_param('is', $userId, $pin_hash) && $stmt->execute() && $stmt->bind_result($row_pin) && $stmt->fetch()) {
$this->setUserPinFailed($userId, 0);
return $pin_hash === $row_pin;
}
$this->incUserPinFailed($userId);
// Check if this account should be locked
if (isset($this->config['maxfailed']['pin']) && $this->getUserPinFailed($userId) >= $this->config['maxfailed']['pin']) {
$this->changeLocked($userId);
$this->logoutUser();
}
return false;
}
/**
@ -641,3 +661,4 @@ $user->setMail($mail);
$user->setToken($oToken);
$user->setBitcoin($bitcoin);
$user->setSetting($setting);
$user->setErrorCodes($aErrorCodes);

View File

@ -69,6 +69,25 @@ $config['wallet']['host'] = 'localhost:19334';
$config['wallet']['username'] = 'testnet';
$config['wallet']['password'] = 'testnet';
/**
* Lock account after maximum failed logins
*
* Explanation:
* To avoid accounts being hacked by brute force attacks,
* set a maximum amount of failed login or pin entry attempts before locking
* the account. They will need to contact site support to re-enable the account.
*
* This also applies for invalid PIN entries, which is covered by the pin option.
*
* Workers are not affected by this lockout, mining will continue as usual.
*
* Default:
* login = 3
* pin = 3
**/
$config['maxfailed']['login'] = 3;
$config['maxfailed']['pin'] = 3;
/**
* Getting Started Config
*

View File

@ -6,7 +6,7 @@ if (!defined('SECURITY'))
if ($user->isAuthenticated()) {
if ( ! $user->checkPin($_SESSION['USERDATA']['id'], @$_POST['authPin']) && @$_POST['do']) {
$_SESSION['POPUP'][] = array('CONTENT' => 'Invalid PIN','TYPE' => 'errormsg');
$_SESSION['POPUP'][] = array('CONTENT' => 'Invalid PIN. ' . ($config['maxfailed']['pin'] - $user->getUserPinFailed($_SESSION['USERDATA']['id'])) . ' attempts remaining.', 'TYPE' => 'errormsg');
} else {
switch (@$_POST['do']) {
case 'cashOut':

View File

@ -14,7 +14,12 @@ $aRoundShares = $statistics->getRoundShares();
switch (@$_POST['do']) {
case 'lock':
$supress_master = 1;
// Reset user account
$user->changeLocked($_POST['account_id']);
if ($user->isLocked($_POST['account_id']) == 0) {
$user->setUserFailed($_POST['account_id'], 0);
$user->setUserPinFailed($_POST['account_id'], 0);
}
break;
case 'fee':
$supress_master = 1;

View File

@ -0,0 +1 @@
ALTER TABLE `accounts` ADD `failed_pins` INT NOT NULL AFTER `failed_logins` ;