summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGeekologist <Conver@users.noreply.github.com>2015-09-13 16:49:03 +0200
committerGeekologist <Conver@users.noreply.github.com>2015-09-13 16:49:03 +0200
commit5657e2d67970700ea61bba4fefcb476554160dc0 (patch)
treed1349181ec41d07f51cc38cb306998b83af3882e
parentb211a5bd83b19f54673264294190312b5f21bc87 (diff)
parent34cc077ccd7bf4fde18f6efd27ace99326127698 (diff)
downloadPHPAuth-5657e2d67970700ea61bba4fefcb476554160dc0.zip
PHPAuth-5657e2d67970700ea61bba4fefcb476554160dc0.tar.gz
PHPAuth-5657e2d67970700ea61bba4fefcb476554160dc0.tar.bz2
Merge pull request #99 from JacquesLoubser/master
Advanced Attack Mitigation
-rw-r--r--.gitignore9
-rw-r--r--README.md61
-rwxr-xr-xauth.class.php249
-rw-r--r--database.sql6
-rwxr-xr-xlanguages/en_GB.php1
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
diff --git a/README.md b/README.md
index 3cdb2a8..392e091 100644
--- a/README.md
+++ b/README.md
@@ -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.";