From 2dab51cab3d2d1b4fdd0a126f4118d550b5ce868 Mon Sep 17 00:00:00 2001 From: bernd Date: Thu, 4 Mar 2021 13:46:42 +0100 Subject: [PATCH 1/3] ip des requests wird hexadezimal gespeichert --- index.php | 8 +++----- lib/db.php | 51 +++++++++++++++++++++++++++++++++----------------- validation.php | 2 +- 3 files changed, 38 insertions(+), 23 deletions(-) diff --git a/index.php b/index.php index a6393a7..752a2c9 100644 --- a/index.php +++ b/index.php @@ -1,6 +1,6 @@ diff --git a/lib/db.php b/lib/db.php index acd5e41..01afeba 100644 --- a/lib/db.php +++ b/lib/db.php @@ -7,6 +7,10 @@ * desc: Anbindung an die (Postgres) Datenbank. */ +error_reporting(E_ALL); +ini_set("display_errors", "on"); +ini_set("display_startip_errors", "on"); + if (!defined('INCLUDES_ALLOWED')) die('Access denied.'); @@ -154,23 +158,27 @@ class Database { public function createTable(): bool { /** - * Erstellt die Tabelle Requests. + * Erstellt die Tabelle Requests. Wir speichern die IP als 16 Byte + * Binary. Damit soll später ein gewisser Schutz gegen Spammer + * erreicht werden. (Wie viele Requests innerhalb welcher Zeit) */ - $this->log->n("try to create table requests"); + $this->log->n("Try to create table requests"); $stmt = "CREATE TABLE IF NOT EXISTS requests ( id serial PRIMARY KEY, nick varchar(80) NOT NULL UNIQUE, email varchar(80) NOT NULL, token char(32) NOT NULL UNIQUE, + ip bytea, time integer NOT NULL);"; try { $this->pdo->exec($stmt); } catch (PDOException $e) { - $this->log-e("Failed to create table requests"); + $this->log->e("Failed to create table requests"); + $this->log->e("Error: {$e->getMessage()}"); return false; } - $this->log-n("Table requests successfull created"); + $this->log->n("Table requests successfull created"); return true; } @@ -305,12 +313,16 @@ class Database { public function saveRequest($token): bool { /** - * Speichert den gewünschten Nick, die Emailadresse, das Token und - * einen Zeitstempel in der Tabelle Requests. + * Speichert den gewünschten Nick, die Emailadresse, das Token, die + * IP und einen Zeitstempel in der Tabelle Requests. + * TODO: IP nicht Hexadezimal, sondern Binär speichern. Spart Platz + * und ist schneller. Bin ich leider zu blöd für. * TODO: Sollten/Müssen Nick und Email noch durch htmlspecialchars() * oder reichen die prepared Statments? */ + $bin = inet_pton($_SERVER['REMOTE_ADDR']); + $ip = bin2hex($bin); $nick = $_POST['login']; $email = $_POST['email']; date_default_timezone_set("Europe/Berlin"); @@ -318,21 +330,26 @@ class Database { $this->log->d("Save request for: {$nick} with {$token} at {$time}"); try { $stmt = $this->pdo->prepare("INSERT INTO requests - (nick, email, token, time) VALUES - (:nick, :email, :token, :time)"); - $response = $stmt->execute(array(':nick' => $nick, - ':email' => $email, - ':token' => $token, - ':time' => $time)); + (nick, email, token, ip, time) VALUES + (:nick, :email, :token, :ip, :time)"); + $stmt->BindValue(':nick', $nick); + $stmt->BindValue(':email', $email); + $stmt->BindValue(':token', $token); + $stmt->BindValue(':ip', $ip, PDO::PARAM_LOB); + $stmt->BindValue(':time', $time); + $response = $stmt->execute(); } catch (PDOException $e) { - $errmsg = $e->getMessage(); $this->log->e("Saving request failed"); - $this->log->e("Error: {$errmsg}"); + $this->log->e("Error: {$e->getMessage()}"); return false; } - $this->log->i("Request saved successfull"); - $this->log->d("Database returns: {$response}"); - return true; + if ($response === 1) { + $this->log->i("Request saved successfull"); + return true; + } else { + $this->log->e("Database returns: {$response}"); + } + return false; } public function getToken(): array { diff --git a/validation.php b/validation.php index 65f6551..c64ff5c 100644 --- a/validation.php +++ b/validation.php @@ -1,6 +1,6 @@ Date: Thu, 4 Mar 2021 18:29:18 +0100 Subject: [PATCH 2/3] =?UTF-8?q?kleine=20=C3=A4nderungen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/register.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/register.php b/lib/register.php index 9c04ee7..d3ccc72 100644 --- a/lib/register.php +++ b/lib/register.php @@ -114,12 +114,11 @@ class Registrator extends BaseClass { private function removeRequest(): bool { /** - * Läßt den Request aus der tabelle requests entfernen. + * Läßt den Request aus der Tabelle requests entfernen. */ $id = $this->dataSet[0]['id']; $nick = $this->dataSet[0]['nick']; - $token = $this->dataSet[0]['token']; try { $response = $this->db->removeRequest($id); } catch (Exception $e) { From 4ffaf6f54d2c55a67906ddd53a80e6d5cf053b15 Mon Sep 17 00:00:00 2001 From: bernd Date: Thu, 4 Mar 2021 19:25:48 +0100 Subject: [PATCH 3/3] =?UTF-8?q?funktionen=20in=20common.php=20ausgelagert,?= =?UTF-8?q?=20treiberweichen=20f=C3=BCr=20datenbankabfragen,=20spamschutz?= =?UTF-8?q?=20hinzugef=C3=BCgt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/common.php | 67 ++++++++++++++++++++++ lib/db.php | 105 +++++++++++++++++++--------------- lib/request.php | 147 ++++++++++++++++++++++++++++++++---------------- 3 files changed, 226 insertions(+), 93 deletions(-) create mode 100644 lib/common.php diff --git a/lib/common.php b/lib/common.php new file mode 100644 index 0000000..878b45e --- /dev/null +++ b/lib/common.php @@ -0,0 +1,67 @@ +pdo = $pdo; @@ -134,14 +140,18 @@ class Database { /** * Kontrolliert, ob eine Tabelle zum Eintragen der Requests * vorhanden ist. - * TODO: Die Query ist Postgres spezifisch. Treiber durchreichen und - * eine Weiche anlegen? */ $this->log->d("Check if table requests exists"); - $stmt = "SELECT * FROM information_schema.tables WHERE + if ($this->pdo->driver === "PDO_PGSQL") { + $stmt = "SELECT * FROM information_schema.tables WHERE table_type = 'BASE TABLE' and table_name = 'requests'"; + } else if ($this->pdo->driver === "PDO_PSQLITE") { + // ungetestet + $stmt = "SELECT 1 FROM sqlite_master WHERE type='table' and + name='requests'"; + } try { $response = $this->pdo->query($stmt); } catch (PDOException $e) { @@ -164,13 +174,24 @@ class Database { */ $this->log->n("Try to create table requests"); - $stmt = "CREATE TABLE IF NOT EXISTS requests ( + if ($this->pdo->driver === "PDO_PGSQL") { + $stmt = "CREATE TABLE IF NOT EXISTS requests ( id serial PRIMARY KEY, nick varchar(80) NOT NULL UNIQUE, email varchar(80) NOT NULL, token char(32) NOT NULL UNIQUE, ip bytea, time integer NOT NULL);"; + } else if ($this->pdo->driver === "PDO_PSQLITE") { + // ungetestet + $stmt = "CREATE TABLE IF NOT EXISTS request ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + nick TEXT NUT NULL UNIQUE; + email TEXT NOT NULL, + token TEXT NOT NULL UNIQUE, + ip BLOB, + time INTEGER NOT NULL);"; + } try { $this->pdo->exec($stmt); } catch (PDOException $e) { @@ -191,7 +212,8 @@ class Database { * Sollte diese eine Exception werfen, wird true (es gibt einen * Benutzer) zurück gegeben. Ansonsten wird der Rückgabewert * von der Funktion getNick ausgewertet, die aus der übergebenen - * Matrix-ID den localpart extrahiert. + * Matrix-ID den localpart extrahiert. Nutzt für die Suche die + * Klassenfunktion searchUser(). Braucht getNick() common.php! */ $this->log->d("Search for localpart {$nick} in users"); @@ -212,7 +234,7 @@ class Database { else { foreach ($response as $array) { - $uid = $this->getNick($array['name']); + $uid = getNick($array['name']); $this->log->d("Compare {$nick} with {$uid}"); if ($uid === $nick) { $this->log->i("MXID localpart already exists: {$nick}"); @@ -232,7 +254,8 @@ class Database { * Nutzernamen. Die Abfrage der Datenbank wird an die Funktion * searchUser delegiert. Sollte diese eine Exception werfen, wird * true (es gibt einen benutzer) zurück gegeben. Ansonsten wird die - * Anzahl der Treffer ausgewertet. + * Anzahl der Treffer ausgewertet. Nutzt für die Suche die eigene + * Funktion searchUser(). */ $this->log->d("Search for localpart {$nick} in requests"); @@ -245,7 +268,7 @@ class Database { } $count = count($response); if ($count > 0) { - $this->log->d("Search for {$nick}: {$count} hit(s)"); + $this->log->n("Search for {$nick}: {$count} hit(s)"); return true; } $this->log->d("Nothing found"); @@ -278,55 +301,47 @@ class Database { } } - private function getNick(string $mid): string - { + public function getTimestamps(): array { + /** - * Extrahiert aus einer Matrix-ID den localpart. - * TODO: In eine bibliothek auslagern? (/lib/common) + * Schaut in der Datenbank, ob es bereits Einträge mit der aktuellen + * Remote IP gibt und gibt alle Einträge des dazu gehörendem + * Zeitstempels zurück. Braucht getRemoteHexIP() aus common.php! + * TODO: flexibler gestalten? IP als Parameter übergeben? */ - - $this->log->d("Extract nick from {$mid}"); - $uid = ""; - $append = false; - $strarray = str_split($mid); - foreach ($strarray as $char) - { - if ($char == '@') - { - $append = true; - } - else if ($char == ':') - { - $this->log->d("Extracted: {$uid}"); - return $uid; - } - else - { - if ($append === true) - { - $uid = $uid.$char; - } - } + + $ip = getRemoteHexIP(); + $this->log->i("Search for IP: {$_SERVER['REMOTE_ADDR']}"); + $stmt = $this->pdo->prepare("SELECT time FROM requests WHERE + ip = :ip"); + try { + $stmt->BindValue(':ip', $ip, PDO::PARAM_LOB); + $stmt->execute(); + $response = $stmt->fetchAll(); + } catch (PDOException $e) { + $this->log->e("getIPs: ERROR: {$e->getMessage()}"); } + $count = count($response); + $this->log->d("{$count} Timestamps found");; + return $response; } public function saveRequest($token): bool { /** * Speichert den gewünschten Nick, die Emailadresse, das Token, die - * IP und einen Zeitstempel in der Tabelle Requests. + * IP und einen Zeitstempel in der Tabelle Requests. Braucht die + * Funktionen getRemoteHexIP() und getNow() aus common.php. * TODO: IP nicht Hexadezimal, sondern Binär speichern. Spart Platz * und ist schneller. Bin ich leider zu blöd für. * TODO: Sollten/Müssen Nick und Email noch durch htmlspecialchars() * oder reichen die prepared Statments? */ - $bin = inet_pton($_SERVER['REMOTE_ADDR']); - $ip = bin2hex($bin); + $ip = getRemoteHexIP(); $nick = $_POST['login']; $email = $_POST['email']; - date_default_timezone_set("Europe/Berlin"); - $time = time(); + $time = getNow(); $this->log->d("Save request for: {$nick} with {$token} at {$time}"); try { $stmt = $this->pdo->prepare("INSERT INTO requests @@ -343,11 +358,9 @@ class Database { $this->log->e("Error: {$e->getMessage()}"); return false; } - if ($response === 1) { - $this->log->i("Request saved successfull"); + if ($response) { + $this->log->n("Request saved successfull"); return true; - } else { - $this->log->e("Database returns: {$response}"); } return false; } diff --git a/lib/request.php b/lib/request.php index bb8941b..1d46e4b 100644 --- a/lib/request.php +++ b/lib/request.php @@ -13,6 +13,7 @@ if (!defined('INCLUDES_ALLOWED')) require("base.php"); require("static/mail.php"); +require_once("common.php"); class Request extends BaseClass { @@ -52,6 +53,9 @@ class Request extends BaseClass { } else if (false === $this->checkUser()) { $message = "User Id is already taken"; return false; + } else if (false === $this->checkRequests()) { + $message = "Too many requests"; + return false; } else { if ($this->generateToken(16) === true) { if ($this->saveRequest() === true) { @@ -67,53 +71,6 @@ class Request extends BaseClass { return false; } - - private function saveRequest(): bool { - - /** - * Veranlaßt die Speicherung der Anfrage in der Tabelle requests. - * Bekommt aus der Datenbank (auch im Falle einer PDO Exception) - * einen Boolean zurück. - */ - - try { - $response = $this->db->saveRequest($this->token); - } catch (Exception $e) { - $this->log->e("Error: Database returns: {$e->getMessage()}"); - } - if ($response === true) { - return true; - } - return false; - } - - private function sendVerificationMail(): bool { - - /** - * Verschickt die Mail mit dem Verifizierungslink. - * TODO: Reicht filter_input()? Was kann hier passieren? - */ - - $mailTo = filter_input(INPUT_POST, 'email', FILTER_SANITIZE_EMAIL); - $mxdomain = $this->config->getMxDomain(); - $baseurl = $this->config->getBaseURL(); - $validator = $this->config->getValidator(); - $mailFrom = $this->config->getMailFrom(); - $mailSubject = $this->config->getMailSubject(); - $mailClosure = $this->config->getMailClosure(); - $this->log->d("Try to send verification mail"); - - $link = $baseurl . $validator . $this->token . "\r\n\r\n"; - $mailbody = MAILTEXT1 . " {$mxdomain} " . MAILTEXT2 . "\r\n\r\n" . $link . $mailClosure; - if (mail($mailTo, $mailSubject, $mailbody, $mailFrom)) - { - $this->log->i("Validationmail successfull send"); - return true; - } - $this->log->e("Sending validation mail failed"); - return false; - } - private function checkCaptcha(): bool { /** @@ -205,6 +162,102 @@ class Request extends BaseClass { return true; } + private function checkRequests(): bool { + + /** + * Prüft, ob für es von der aktuellen Remote IP bereits Anfragen + * gibt. Diese sollten gewisse Limits nicht überschreiten. + */ + + $timestamps = $this->db->getTimestamps(); + $now = getNow(); + + // Wenn es der erste Request ist -> return true + $count = count($timestamps); + if ($count === 0) { + $this->log->i("First request from {$_SERVER['REMOTE_ADDR']}"); + return true; + } + $first = $timestamps[array_key_first($timestamps)]['time']; + $last = $timestamps[array_key_last($timestamps)]['time']; + $duration = $last - $first; + $average = $duration / $count; + + // Maximal 10 Anfragen in 24 Stunden + if ($count > 10 && $duration < 86400) { + $this->log->n("To many requests in 24 hours"); + return false; + } + + // Wenn der letzte Request weniger als 10 Sekunden her ist -> return + // false + if (($now - $last) < 5) { + $this->log->n("Time between two requests under 5 seconds"); + return false; + } + + // Wenn die durchschnittliche Dauer zwischen allen Anfragen unter 30 + // Sekunden liegt -> return false + if (($average) < 30 && $count > 3) { + $this->log->n("Duration between all requests under 10 seconds"); + return false; + } + + // nicht mehr als 20 Anfragen von einer IP in der Datenbank + // speichern -> return false + if ($count > 20) { + $this->log->n("To many requests in database"); + return false; + } + return true; + } + + private function saveRequest(): bool { + + /** + * Veranlaßt die Speicherung der Anfrage in der Tabelle requests. + * Bekommt aus der Datenbank (auch im Falle einer PDO Exception) + * einen Boolean zurück. + */ + + try { + $response = $this->db->saveRequest($this->token); + } catch (Exception $e) { + $this->log->e("Error: Database returns: {$e->getMessage()}"); + } + if ($response === true) { + return true; + } + return false; + } + + private function sendVerificationMail(): bool { + + /** + * Verschickt die Mail mit dem Verifizierungslink. + * TODO: Reicht filter_input()? Was kann hier passieren? + */ + + $mailTo = filter_input(INPUT_POST, 'email', FILTER_SANITIZE_EMAIL); + $mxdomain = $this->config->getMxDomain(); + $baseurl = $this->config->getBaseURL(); + $validator = $this->config->getValidator(); + $mailFrom = $this->config->getMailFrom(); + $mailSubject = $this->config->getMailSubject(); + $mailClosure = $this->config->getMailClosure(); + $this->log->d("Try to send verification mail"); + + $link = $baseurl . $validator . $this->token . "\r\n\r\n"; + $mailbody = MAILTEXT1 . " {$mxdomain} " . MAILTEXT2 . "\r\n\r\n" . $link . $mailClosure; + if (mail($mailTo, $mailSubject, $mailbody, $mailFrom)) + { + $this->log->i("Validationmail successfull send"); + return true; + } + $this->log->e("Sending validation mail failed"); + return false; + } + } ?>