$ git clone http://thingshare.ion.nu/thingshare.git
commit 9dc0581959233a2c8ddd09256644c2861a18c84e
Author: Alicia <...>
Date:   Fri Mar 27 23:07:49 2020 +0100

    Added bruteforce protection on login.

diff --git a/db.php b/db.php
index 40c6193..1715fd6 100644
--- a/db.php
+++ b/db.php
@@ -119,6 +119,7 @@ function db_create_tables()
     msgread boolean,
     latest boolean);');
   mysqli_query($db, 'create table userblocks(user integer, blocked text);');
+  mysqli_query($db, 'create table loginfails(ip varchar(256), timestamp datetime);');
 }
 
 function db_getuser($id)
diff --git a/login.php b/login.php
index 537f91a..054d4a9 100644
--- a/login.php
+++ b/login.php
@@ -17,31 +17,53 @@
     You should have received a copy of the GNU Affero General Public License
     along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
-// TODO: Protect against excess failed logins from the same IP
 if(isset($_POST['user']) && isset($_POST['pass']))
 {
   include_once('db.php');
-  $error=_('Incorrect username/password');
-  $user=mysqli_real_escape_string($db, $_POST['user']);
-  $res=mysqli_query($db, 'select salt, password, id, status from users where name="'.$user.'"');
-  if($res=mysqli_fetch_assoc($res))
+  // Check for bruteforcing attempts
+  $fail_time=getoption('loginfail_time', 3600);
+  $fail_limit=getoption('loginfail_limit', 10);
+  $fail_lockout=getoption('loginfail_lockout', 3600*24);
+  $ip=mysqli_real_escape_string($db, $_SERVER['REMOTE_ADDR']);
+  $oldtime=mysqli_real_escape_string($db, date('Y-m-d H:i:s', time()-$fail_time));
+  mysqli_query($db, 'delete from loginfails where timestamp<"'.$oldtime.'"');
+  $res=mysqli_query($db, 'select count(*) from loginfails where ip="'.$ip.'"');
+  $res=mysqli_fetch_row($res);
+  $fail_count=$res[0];
+  if($fail_count>$fail_limit)
   {
-    $hash=explode(':', $res['password']);
-    $pass=hash($hash[0], $_POST['pass'].$res['salt']);
-    if($pass==$hash[1])
+    $error=_('Login attempt limit exceeded');
+  }else{
+    // Check password
+    $error=_('Incorrect username/password');
+    $user=mysqli_real_escape_string($db, $_POST['user']);
+    $res=mysqli_query($db, 'select salt, password, id, status from users where name="'.$user.'"');
+    if($res=mysqli_fetch_assoc($res))
     {
-      switch($res['status'])
+      $hash=explode(':', $res['password']);
+      $pass=hash($hash[0], $_POST['pass'].$res['salt']);
+      if($pass==$hash[1])
       {
-        case ACCOUNT_ACTIVE:
-          session_start();
-          $_SESSION['name']=$_POST['user'];
-          $_SESSION['id']=$res['id'];
-          header('Location: '.(isset($_GET['returnto'])?urldecode($_GET['returnto']):BASEURL));
-          exit();
-        case ACCOUNT_BANNED: $error=_('Banned'); break;
-        case ACCOUNT_EMAILUNVERIFIED: $error=_('Please check for a verification e-mail'); break;
+        switch($res['status'])
+        {
+          case ACCOUNT_ACTIVE:
+            session_start();
+            $_SESSION['name']=$_POST['user'];
+            $_SESSION['id']=$res['id'];
+            header('Location: '.(isset($_GET['returnto'])?urldecode($_GET['returnto']):BASEURL));
+            exit();
+          case ACCOUNT_BANNED: $error=_('Banned'); break;
+          case ACCOUNT_EMAILUNVERIFIED: $error=_('Please check for a verification e-mail'); break;
+        }
       }
     }
+    $time=mysqli_real_escape_string($db, date('Y-m-d H:i:s'));
+    mysqli_query($db, 'insert into loginfails(ip, timestamp) values("'.$ip.'", "'.$time.'")');
+    if($fail_count==$fail_limit) // Limit reached, apply lockout
+    {
+      $time=mysqli_real_escape_string($db, date('Y-m-d H:i:s', time()-$fail_time+$fail_lockout));
+      mysqli_query($db, 'update loginfails set timestamp="'.$time.'" where ip="'.$ip.'"');
+    }
   }
   $error='<div class="error">'.$error.'</div>';
 }else{
diff --git a/search.php b/search.php
index 7ed4e2b..ca6ef45 100644
--- a/search.php
+++ b/search.php
@@ -81,7 +81,7 @@ foreach($results as $thing)
 }
 ?>
 <div class="sidebar">
-  <form>
+  <form action="<?=BASEURL?>/search">
     <input type="hidden" name="q" value="<?=(isset($_GET['q'])?$_GET['q']:'')?>" />
     <?=_('Results per page (approximately):')?> <input type="number" name="perpage" value="<?=$perpage?>" /><br />
     <?=_('Sort by:')?> <select name="sort">