Skip to content

Commit 1889635

Browse files
committed
add login rate limiting, fixes #2
1 parent 5d2534a commit 1889635

3 files changed

Lines changed: 67 additions & 0 deletions

File tree

init.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ function initDatabase() {
4040
password TEXT NOT NULL,
4141
data TEXT
4242
)',
43+
'CREATE TABLE IF NOT EXISTS ipAttempts (
44+
ip VARCHAR(255) NOT NULL PRIMARY KEY,
45+
type VARCHAR(255) NOT NULL,
46+
expires NOT NULL
47+
)',
4348
];
4449

4550
try {

lib/IpAttempts.php

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php
2+
namespace Pdsinterop\PhpSolid;
3+
4+
class IpAttempts {
5+
private static $pdo;
6+
private static function connect() {
7+
if (!isset(self::$pdo)) {
8+
self::$pdo = new \PDO("sqlite:" . DBPATH);
9+
}
10+
}
11+
12+
public static function logFailedAttempt($ip, $type, $expires) {
13+
self::connect();
14+
15+
$query = self::$pdo->prepare(
16+
'INSERT INTO ipAttempts VALUES(:ip, :type, :expires)'
17+
);
18+
$query->execute([
19+
':ip' => $ip,
20+
':type' => $type,
21+
':expires' => $expires->getTimestamp()
22+
]);
23+
}
24+
25+
public static function getAttemptsCount($ip, $type) {
26+
self::connect();
27+
28+
$now = new \DateTime();
29+
$query = self::$pdo->prepare(
30+
'SELECT count(ip) as count FROM ipAttempts WHERE ip=:ip AND type=:type AND expires > :now'
31+
);
32+
$query->execute([
33+
':ip' => $ip,
34+
':type' => $type,
35+
':now' => $now->getTimestamp()
36+
]);
37+
$result = $query->fetch();
38+
if (isset($result['count'])) {
39+
return $result['count'];
40+
}
41+
return 0;
42+
}
43+
public static function cleanupAttempts() {
44+
self::connect();
45+
46+
$now = new \DateTime();
47+
$query = self::$pdo->prepare(
48+
'DELETE FROM ipAttempts WHERE expires < :now'
49+
);
50+
$query->execute([
51+
':now' => $now->getTimestamp()
52+
]);
53+
}
54+
}

www/idp/index.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use Pdsinterop\PhpSolid\ClientRegistration;
1010
use Pdsinterop\PhpSolid\User;
1111
use Pdsinterop\PhpSolid\Mailer;
12+
use Pdsinterop\PhpSolid\IpAttempts;
1213

1314
$request = explode("?", $_SERVER['REQUEST_URI'], 2)[0];
1415
$method = $_SERVER['REQUEST_METHOD'];
@@ -284,13 +285,19 @@
284285
break;
285286
case "/login/password":
286287
case "/login/password/":
288+
$failureCount = IpAttempts::getAttemptsCount($_SERVER['REMOTE_ADDR'], "login");
289+
if ($failureCount > 5) {
290+
header("HTTP/1.1 400 Bad Request");
291+
exit();
292+
}
287293
if (User::checkPassword($_POST['username'], $_POST['password'])) {
288294
if (!isset($_POST['redirect_uri']) || $_POST['redirect_uri'] === '') {
289295
header("Location: /dashboard/");
290296
exit();
291297
}
292298
header("Location: " . urldecode($_POST['redirect_uri'])); // FIXME: Do we need to harden this?
293299
} else {
300+
IpAttempts::logFailedAttempt($_SERVER['REMOTE_ADDR'], "login", time() + 3600);
294301
header("Location: /login/");
295302
}
296303
break;
@@ -405,6 +412,7 @@
405412
if (!file_exists(CLEANUP_FILE) || (filemtime(CLEANUP_FILE) < time())) {
406413
touch(CLEANUP_FILE, time() + 3600);
407414
User::cleanupTokens();
415+
IpAttempts::cleanupAttempts();
408416
}
409417
break;
410418
case "OPTIONS":

0 commit comments

Comments
 (0)