diff options
author | Geekologist <Conver@users.noreply.github.com> | 2015-09-13 16:49:03 +0200 |
---|---|---|
committer | Geekologist <Conver@users.noreply.github.com> | 2015-09-13 16:49:03 +0200 |
commit | 5657e2d67970700ea61bba4fefcb476554160dc0 (patch) | |
tree | d1349181ec41d07f51cc38cb306998b83af3882e | |
parent | b211a5bd83b19f54673264294190312b5f21bc87 (diff) | |
parent | 34cc077ccd7bf4fde18f6efd27ace99326127698 (diff) | |
download | PHPAuth-5657e2d67970700ea61bba4fefcb476554160dc0.zip PHPAuth-5657e2d67970700ea61bba4fefcb476554160dc0.tar.gz PHPAuth-5657e2d67970700ea61bba4fefcb476554160dc0.tar.bz2 |
Merge pull request #99 from JacquesLoubser/master
Advanced Attack Mitigation
-rw-r--r-- | .gitignore | 9 | ||||
-rw-r--r-- | README.md | 61 | ||||
-rwxr-xr-x | auth.class.php | 249 | ||||
-rw-r--r-- | database.sql | 6 | ||||
-rwxr-xr-x | languages/en_GB.php | 1 |
5 files changed, 222 insertions, 104 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..98e1eeb --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +.idea/.name +.idea/misc.xml +.idea/encodings.xml +.idea/vcs.xml +.idea/modules.xml +.idea/scopes/scope_settings.xml +.idea/PHPAuth.iml +.idea/workspace.xml +.idea/sqldialects.xml
\ No newline at end of file @@ -12,7 +12,7 @@ Features * Uses [bcrypt](http://en.wikipedia.org/wiki/Bcrypt) to hash passwords, a secure algorithm that uses an expensive key setup phase * Uses an individual 128 bit salt for each user, pulled from /dev/urandom, making rainbow tables useless * Uses PHP's [PDO](http://php.net/manual/en/book.pdo.php) database interface and uses prepared statements meaning an efficient system, resilient against SQL injection -* Blocks attackers by IP for any defined time after any amount of failed actions on the portal +* Blocks (or verifies) attackers by IP for any defined time after any amount of failed actions on the portal * No plain text passwords are sent or stored by the system * Integrates easily into most existing websites, and can be a great starting point for new projects * Easy configuration of multiple system parameters @@ -25,9 +25,9 @@ User actions * Register * Activate account * Resend activation email -* Reset password
+* Reset password * Change password -* Change email address
+* Change email address * Delete account * Logout @@ -64,16 +64,57 @@ The database table `config` contains multiple parameters allowing you to configu * `smtp_username` : the username for the SMTP server * `smtp_password` : the password for the SMTP server * `smtp_port` : the port for the SMTP server -* `smtp_security` : `NULL` for no encryption, `tls` for TLS encryption, `ssl` for SSL encryption
-* `verify_password_min_length` : minimum password length, default is `3`
-* `verify_password_max_length` : maximum password length, default is `150`
-* `verify_password_strong_requirements` : use strong password requirments (at least one uppercase and lowercase character, and at least one digit), default is `1` (`true`)
-* `verify_email_min_length` : minimum EMail length, default is `5`
-* `verify_email_max_length` : maximum EMail length, default is `100`
-* `verify_email_use_banlist` : use banlist while checking allowed EMails (see `/files/domains.json`), default is `1` (`true`) +* `smtp_security` : `NULL` for no encryption, `tls` for TLS encryption, `ssl` for SSL encryption +* `verify_password_min_length` : minimum password length, default is `3` +* `verify_password_max_length` : maximum password length, default is `150` +* `verify_password_strong_requirements` : use strong password requirments (at least one uppercase and lowercase character, and at least one digit), default is `1` (`true`) +* `verify_email_min_length` : minimum EMail length, default is `5` +* `verify_email_max_length` : maximum EMail length, default is `100` +* `verify_email_use_banlist` : use banlist while checking allowed EMails (see `/files/domains.json`), default is `1` (`true`) +* `attack_mitigation_time` : time used for rolling attempts timeout, default is `+30 minutes`. Must respect PHP's [strtotime](http://php.net/manual/en/function.strtotime.php) format. +* `attempts_before_verify` : maximum amount of attempts to be made within `attack_mitigation_time` before requiring captcha. Default is `5` +* `attempt_before_block` : maximum amount of attempts to be made within `attack_mitigation_time` before temporally blocking the IP address. Defualt is `30` The rest of the parameters generally do not need changing. +CAPTCHA Implementation +--------------- + +If `isBlocked()` returns `verify`, then a CAPTCHA code should be displayed. +The method `checkCaptcha($captcha)` is called to verify a CAPTCHA code. By default this method returns `true`, but should be overridden to verify a CAPTCHA. + +For example, if you are using Google's ReCaptcha NoCaptcha, use the following code: + +```php + private function checkCaptcha($captcha) + { + try { + + $url = 'https://www.google.com/recaptcha/api/siteverify'; + $data = ['secret' => 'your_secret_here', + 'response' => $captcha, + 'remoteip' => $_SERVER['REMOTE_ADDR']]; + + $options = [ + 'http' => [ + 'header' => "Content-type: application/x-www-form-urlencoded\r\n", + 'method' => 'POST', + 'content' => http_build_query($data) + ] + ]; + + $context = stream_context_create($options); + $result = file_get_contents($url, false, $context); + return json_decode($result)->success; + } + catch (\Exception $e) { + return false; + } +} +``` + +If a CAPTCHA is not to be used, please ensure to set `attempt_before_block` to the same value as `attempts_before_verify`. + How to secure a page --------------- diff --git a/auth.class.php b/auth.class.php index cf9edcf..f9aedd5 100755 --- a/auth.class.php +++ b/auth.class.php @@ -36,18 +36,27 @@ class Auth * @param string $email * @param string $password * @param int $remember + * @param string $captcha = NULL * @return array $return */ - public function login($email, $password, $remember = 0) + public function login($email, $password, $remember = 0, $captcha = NULL) { $return['error'] = true; - if ($this->isBlocked()) { - $return['message'] = $this->lang["user_blocked"]; - - return $return; - } + $block_status = $this->isBlocked(); + if($block_status == "verify") + { + if($this->checkCaptcha($captcha) == false) + { + $return['message'] = $this->lang["user_verify_failed"]; + return $return; + } + } + if ($block_status == "block") { + $return['message'] = $this->lang["user_blocked"]; + return $return; + } $validateEmail = $this->validateEmail($email); $validatePassword = $this->validatePassword($password); @@ -116,17 +125,27 @@ class Auth * @param string $password * @param string $repeatpassword * @param array $params + * @param string $captcha = NULL * @return array $return */ - public function register($email, $password, $repeatpassword, $params = Array()) + public function register($email, $password, $repeatpassword, $params = Array(), $captcha = NULL) { $return['error'] = true; - if ($this->isBlocked()) { - $return['message'] = $this->lang["user_blocked"]; - return $return; - } + $block_status = $this->isBlocked(); + if($block_status == "verify") + { + if($this->checkCaptcha($captcha) == false) + { + $return['message'] = $this->lang["user_verify_failed"]; + return $return; + } + } + if ($block_status == "block") { + $return['message'] = $this->lang["user_blocked"]; + return $return; + } if ($password !== $repeatpassword) { $return['message'] = $this->lang["password_nomatch"]; @@ -177,10 +196,11 @@ class Auth { $return['error'] = true; - if($this->isBlocked()) { - $return['message'] = $this->lang["user_blocked"]; - return $return; - } + $block_status = $this->isBlocked(); + if ($block_status == "block") { + $return['message'] = $this->lang["user_blocked"]; + return $return; + } if(strlen($key) !== 20) { $this->addAttempt(); @@ -224,11 +244,11 @@ class Auth public function requestReset($email) { $return['error'] = true; - - if ($this->isBlocked()) { - $return['message'] = $this->lang["user_blocked"]; - return $return; - } + $block_status = $this->isBlocked(); + if ($block_status == "block") { + $return['message'] = $this->lang["user_blocked"]; + return $return; + } $validateEmail = $this->validateEmail($email); @@ -383,10 +403,11 @@ class Auth { $ip = $this->getIp(); - if ($this->isBlocked()) { - return false; - } - + $block_status = $this->isBlocked(); + if ($block_status == "block") { + $return['message'] = $this->lang["user_blocked"]; + return false; + } if (strlen($hash) != 40) { return false; } @@ -552,17 +573,27 @@ class Auth * Allows a user to delete their account * @param int $uid * @param string $password + * @param string $captcha = NULL * @return array $return */ - public function deleteUser($uid, $password) + public function deleteUser($uid, $password, $captcha = NULL) { $return['error'] = true; - if ($this->isBlocked()) { - $return['message'] = $this->lang["user_blocked"]; - return $return; - } + $block_status = $this->isBlocked(); + if($block_status == "verify") + { + if($this->checkCaptcha($captcha) == false) + { + $return['message'] = $this->lang["user_verify_failed"]; + return $return; + } + } + if ($block_status == "block") { + $return['message'] = $this->lang["user_blocked"]; + return $return; + } $validatePassword = $this->validatePassword($password); @@ -826,17 +857,27 @@ class Auth * @param string $key * @param string $password * @param string $repeatpassword + * @param string $captcha = NULL * @return array $return */ - public function resetPass($key, $password, $repeatpassword) + public function resetPass($key, $password, $repeatpassword, $captcha = NULL) { $return['error'] = true; - if ($this->isBlocked()) { - $return['message'] = $this->lang["user_blocked"]; - return $return; - } + $block_status = $this->isBlocked(); + if($block_status == "verify") + { + if($this->checkCaptcha($captcha) == false) + { + $return['message'] = $this->lang["user_verify_failed"]; + return $return; + } + } + if ($block_status == "block") { + $return['message'] = $this->lang["user_blocked"]; + return $return; + } if(strlen($key) != 20) { $return['message'] = $this->lang["resetkey_invalid"]; @@ -908,11 +949,11 @@ class Auth public function resendActivation($email) { $return['error'] = true; - - if ($this->isBlocked()) { - $return['message'] = $this->lang["user_blocked"]; - return $return; - } + $block_status = $this->isBlocked(); + if ($block_status == "block") { + $return['message'] = $this->lang["user_blocked"]; + return $return; + } $validateEmail = $this->validateEmail($email); @@ -959,17 +1000,27 @@ class Auth * @param int $uid * @param string $currpass * @param string $newpass - * @param $repeatnewpass - * @return $return + * @param string $repeatnewpass + * @param string $captcha = NULL + * @return array $return */ - public function changePassword($uid, $currpass, $newpass, $repeatnewpass) + public function changePassword($uid, $currpass, $newpass, $repeatnewpass, $captcha = NULL) { $return['error'] = true; - if ($this->isBlocked()) { - $return['message'] = $this->lang["user_blocked"]; - return $return; - } + $block_status = $this->isBlocked(); + if($block_status == "verify") + { + if($this->checkCaptcha($captcha) == false) + { + $return['message'] = $this->lang["user_verify_failed"]; + return $return; + } + } + if ($block_status == "block") { + $return['message'] = $this->lang["user_blocked"]; + return $return; + } $validatePassword = $this->validatePassword($currpass); @@ -1021,17 +1072,27 @@ class Auth * @param int $uid * @param string $email * @param string $password + * @param string $captcha = NULL * @return array $return */ - public function changeEmail($uid, $email, $password) + public function changeEmail($uid, $email, $password, $captcha = NULL) { $return['error'] = true; - if ($this->isBlocked()) { - $return['message'] = $this->lang["user_blocked"]; - return $return; - } + $block_status = $this->isBlocked(); + if($block_status == "verify") + { + if($this->checkCaptcha($captcha) == false) + { + $return['message'] = $this->lang["user_verify_failed"]; + return $return; + } + } + if ($block_status == "block") { + $return['message'] = $this->lang["user_blocked"]; + return $return; + } $validateEmail = $this->validateEmail($email); @@ -1086,39 +1147,39 @@ class Auth /** * Informs if a user is locked out - * @return boolean + * @return string */ - private function isBlocked() + public function isBlocked() { $ip = $this->getIp(); - + $this->deleteAttempts($ip, false); $query = $this->dbh->prepare("SELECT count, expiredate FROM {$this->config->table_attempts} WHERE ip = ?"); $query->execute(array($ip)); - if($query->rowCount() == 0) { - return false; - } + $attempts = $query->rowCount(); - $row = $query->fetch(\PDO::FETCH_ASSOC); - - $expiredate = strtotime($row['expiredate']); - $currentdate = strtotime(date("Y-m-d H:i:s")); - - if ($row['count'] == 5) { - if ($currentdate < $expiredate) { - return true; -} - $this->deleteAttempts($ip); - return false; - } + if($attempts < intval($this->config->attempts_before_verify)) + { + return "allow"; + } + if($attempts < intval($this->config->attempts_before_ban)) + { + return "verify"; + } + return "block"; + } - if ($currentdate > $expiredate) { - $this->deleteAttempts($ip); - } - return false; - } + /** + * Verifies a captcha code + * @param string $captcha + * @return boolean + */ + private function checkCaptcha($captcha) + { + return true; + } /** * Adds an attempt to database @@ -1129,36 +1190,41 @@ class Auth { $ip = $this->getIp(); - $query = $this->dbh->prepare("SELECT count FROM {$this->config->table_attempts} WHERE ip = ?"); - $query->execute(array($ip)); + $attempt_expiredate = date("Y-m-d H:i:s", strtotime($this->config->attack_mitigation_time)); - $row = $query->fetch(\PDO::FETCH_ASSOC); + $query = $this->dbh->prepare("INSERT INTO {$this->config->table_attempts} (ip, expiredate) VALUES (?, ?)"); + return $query->execute(array($ip, $attempt_expiredate)); - $attempt_expiredate = date("Y-m-d H:i:s", strtotime("+30 minutes")); - - if (!$row) { - $attempt_count = 1; - - $query = $this->dbh->prepare("INSERT INTO {$this->config->table_attempts} (ip, count, expiredate) VALUES (?, ?, ?)"); - return $query->execute(array($ip, $attempt_count, $attempt_expiredate)); - } - - $attempt_count = $row['count'] + 1; - - $query = $this->dbh->prepare("UPDATE {$this->config->table_attempts} SET count=?, expiredate=? WHERE ip=?"); - return $query->execute(array($attempt_count, $attempt_expiredate, $ip)); } /** * Deletes all attempts for a given IP from database * @param string $ip + * @param boolean $all = false * @return boolean */ - private function deleteAttempts($ip) + private function deleteAttempts($ip, $all = false) { + if($all==true) + { $query = $this->dbh->prepare("DELETE FROM {$this->config->table_attempts} WHERE ip = ?"); return $query->execute(array($ip)); + } + + + $query = $this->dbh->prepare("SELECT count, expiredate FROM {$this->config->table_attempts} WHERE ip = ?"); + $query->execute(array($ip)); + + while ($row = $query->fetch(\PDO::FETCH_ASSOC)) { + $expiredate = strtotime($row['expiredate']); + $currentdate = strtotime(date("Y-m-d H:i:s")); + if($currentdate > $expiredate) + { + $query = $this->dbh->prepare("DELETE FROM {$this->config->table_attempts} WHERE id = ?"); + $query->execute(array($row['id'])); + } + } } /** @@ -1166,7 +1232,6 @@ class Auth * @param int $length * @return string $key */ - public function getRandomKey($length = 20) { $chars = "A1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6Q7R8S9T0U1V2W3X4Y5Z6a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6"; diff --git a/database.sql b/database.sql index 11d84a6..f216cd0 100644 --- a/database.sql +++ b/database.sql @@ -45,13 +45,15 @@ INSERT INTO `config` (`id`, `setting`, `value`) VALUES (29, 'verify_password_strong_requirements', '1'), (30, 'verify_email_min_length', '5'), (31, 'verify_email_max_length', '100'), -(32, 'verify_email_use_banlist', '1'); +(32, 'verify_email_use_banlist', '1'), +(33, 'attack_mitigation_time', '+30 minutes'), +(34, 'attempts_before_verify', '5'), +(35, 'attempts_before_ban', '30'); DROP TABLE IF EXISTS `attempts`; CREATE TABLE `attempts` ( `id` int(11) NOT NULL AUTO_INCREMENT, `ip` varchar(39) NOT NULL, - `count` int(11) NOT NULL, `expiredate` datetime NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; diff --git a/languages/en_GB.php b/languages/en_GB.php index 531ba0f..4ffa4e4 100755 --- a/languages/en_GB.php +++ b/languages/en_GB.php @@ -3,6 +3,7 @@ $lang = array(); $lang['user_blocked'] = "You are currently locked out of the system."; +$lang['user_verify_failed'] = "Captcha Code was invalid."; $lang['email_password_invalid'] = "Email address / password are invalid."; $lang['email_password_incorrect'] = "Email address / password are incorrect."; |