From a8903dcf9abde36a596c70d18ba308f5ef835df5 Mon Sep 17 00:00:00 2001 From: Krombel Date: Sun, 27 May 2018 13:00:48 +0200 Subject: [PATCH 1/3] complete (insecure) password fetching on registration --- README.md | 6 ++++++ database.php | 11 ++++++----- lang/lang.de-de.php | 1 + lang/lang.en-gb.php | 1 + lang/mail.de-de.php | 2 +- lang/mail.en-gb.php | 2 +- public/index.php | 9 ++++++--- public/verify_admin.php | 19 ++++++++++++++++--- 8 files changed, 38 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index fd15305..fa59928 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,12 @@ When using `operationMode=local` you can have the following benefits (some requi To allow the bot to verify the email address of the user and to interact with them e.g. in case of approval this bot needs a running mailserver configuration. This bot relies on php to be properly configured. +### Security: Passwords for registration are stored in clear text +Currently the passwords which are typed in while capturing the register request are stored in clear text. +It is currently strongly recommended to set `"getPasswordOnRegistration" => false` in your config! +This leads to autocreating passwords which will then be send to the users directly +without storing it. + ### Use the ChangePasswortInterceptor (if `operationMode=local`) To allow users to change their pasword you need a reverse proxy which maps `/_matrix/client/r0/account/password` to `internal/intercept_change_password.php`. diff --git a/database.php b/database.php index 6c0c689..f53a5be 100644 --- a/database.php +++ b/database.php @@ -78,7 +78,7 @@ class mxDatabase { first_name TEXT, last_name TEXT, username TEXT, - password_hash TEXT DEFAULT '', + password TEXT DEFAULT '', note TEXT, email TEXT, verify_token TEXT, @@ -184,7 +184,7 @@ class mxDatabase { * * @return ["verify_token"] */ - function addRegistration($first_name, $last_name, $username, $note, $email) { + function addRegistration($first_name, $last_name, $username, $password, $note, $email) { if ($this->userPendingRegistrations($username)) { throw new Exception("USERNAME_PENDING_REGISTRATION"); } @@ -196,8 +196,9 @@ class mxDatabase { $admin_token = bin2hex(random_bytes(16)); $this->db->exec("INSERT INTO registrations - (first_name, last_name, username, note, email, verify_token, admin_token) - VALUES ('" . $first_name . "','" . $last_name . "','" . $username . "','" . $note . "','" + (first_name, last_name, username, password, note, email, verify_token, admin_token) + VALUES ('" . $first_name . "','" . $last_name . "','" + . $username . "','" . $password . "','" . $note . "','" . $email . "','" . $verify_token . "','" . $admin_token . "')"); return [ @@ -217,7 +218,7 @@ class mxDatabase { $res = $this->db->query($sql); if ($res->fetchColumn() > 0) { - $sql = "SELECT first_name, last_name, username, note, email FROM registrations" + $sql = "SELECT first_name, last_name, username, password, note, email FROM registrations" . " WHERE admin_token = '" . $admin_token . "'" . " AND state = " . RegisterState::PendingAdminVerify . " LIMIT 1;"; diff --git a/lang/lang.de-de.php b/lang/lang.de-de.php index 87f4c3f..90cc5ea 100644 --- a/lang/lang.de-de.php +++ b/lang/lang.de-de.php @@ -37,6 +37,7 @@ $language = array( "USERNAME_NOT_ALNUM" => "Nutzername ist nicht alphanumerisch", "USERNAME_PENDING_REGISTRATION" => "Dieser Nutzername wurde bereits zur Registrierung vorgemerkt. Versuche es später noch einmal oder wähle einen anderen Nutzernamen", "USERNAME_REGISTERED" => "Dieser Nutzername wurde bereits registriert. Bitte wähle einen anderen Nutzernamen", + "PASSWORD_NOT_PROVIDED" => "Ein oder beide Passwörter wurden nicht gesetzt", "PASSWORD_NOT_MATCH" => "Passwörter stimmen nicht überein", "NOTE_LENGTH_EXEEDED" => "Notiz ist länger als die erlaubten 50 Zeichen", "PLACEHOLDER_NOTE_ABOUT_YOURSELF" => "Notiz zu dir (max. 50 Zeichen)", diff --git a/lang/lang.en-gb.php b/lang/lang.en-gb.php index 54fae67..43b3bc9 100644 --- a/lang/lang.en-gb.php +++ b/lang/lang.en-gb.php @@ -37,6 +37,7 @@ $language = array( "USERNAME_NOT_ALNUM" => "Username is not alphanumeric", "USERNAME_PENDING_REGISTRATION" => "This username is locked for registration. Try again later or try again with a different username", "USERNAME_REGISTERED" => "This username is already registered. Please try again with another username", + "PASSWORD_NOT_PROVIDED" => "One or both passwords are not provided", "PASSWORD_NOT_MATCH" => "passwords do not match", "NOTE_LENGTH_EXEEDED" => "Note consists of more than 50 characters", "PLACEHOLDER_NOTE_ABOUT_YOURSELF" => "Note about yourself (max. 50 characters)", diff --git a/lang/mail.de-de.php b/lang/mail.de-de.php index f96662a..fe4cfc7 100644 --- a/lang/mail.de-de.php +++ b/lang/mail.de-de.php @@ -79,7 +79,7 @@ Deine Registrierungsanfrage wurde durch die Administratoren bestätigt. Zum Anmelden kannst du folgende Zugangsdaten verwenden: Nutzername: $username -Passwort: $password +Passwort: " . (empty($password) ? "wie selbst gesetzt": $password) . " Hinweis: Das Passwort kannst du aktuell über die App selbst ändern. Auch wenn das Passwort nirgends im Klartext gespeichert wird, kann jemand Zugriff auf diese Mail erlangen und so den Zugriff bekommen. diff --git a/lang/mail.en-gb.php b/lang/mail.en-gb.php index 4f226d5..62f0727 100644 --- a/lang/mail.en-gb.php +++ b/lang/mail.en-gb.php @@ -78,7 +78,7 @@ Your registration request got verified by the admin team. To log in you can use the following credentials:: Username: $username -Password: $password +Passwort: " . (empty($password) ? "as self-set": $password) . " Important: Please change your password as soon as possible after your first login. The password is not stored in clear text on the server but people could get access to this mail diff --git a/public/index.php b/public/index.php index f43ccde..75fe9c3 100644 --- a/public/index.php +++ b/public/index.php @@ -57,8 +57,10 @@ if ($_SERVER["REQUEST_METHOD"] == "POST") { if (ctype_alnum($_POST['username']) != true) { throw new Exception("USERNAME_NOT_ALNUM"); } - if (isset($config["getPasswordOnRegistration"]) && $config["getPasswordOnRegistration"] && - $_POST["password"] != $_POST["password_confirm"]) { + if ($storePassword && (!isset($_POST["password"]) || !isset($_POST["password_confirm"]))) { + throw new Exception("PASSWORD_NOT_PROVIDED"); + } + if ($storePassword && $_POST["password"] != $_POST["password_confirm"]) { throw new Exception("PASSWORD_NOT_MATCH"); } if (isset($_POST["note"]) && strlen($_POST["note"]) > 50) { @@ -82,6 +84,7 @@ if ($_SERVER["REQUEST_METHOD"] == "POST") { } $username = filter_var($_POST["username"], FILTER_SANITIZE_STRING); + $password = ""; if ($storePassword && isset($_POST["password"])) { $password = filter_var($_POST["password"], FILTER_SANITIZE_STRING); } @@ -89,7 +92,7 @@ if ($_SERVER["REQUEST_METHOD"] == "POST") { $email = filter_var($_POST["email"], FILTER_VALIDATE_EMAIL); require_once(__DIR__ . "/../database.php"); - $res = $mx_db->addRegistration($first_name, $last_name, $username, $note, $email); + $res = $mx_db->addRegistration($first_name, $last_name, $username, $password, $note, $email); if (!isset($res["verify_token"])) { error_log("sth. went wrong. registration did not throw but admin_token not set"); diff --git a/public/verify_admin.php b/public/verify_admin.php index 80a3253..3028eee 100644 --- a/public/verify_admin.php +++ b/public/verify_admin.php @@ -71,11 +71,17 @@ try { $mxConn = new MatrixConnection($config["homeserver"], $config["access_token"]); $password = NULL; + $use_db_password = (isset($config["getPasswordOnRegistration"]) && $config["getPasswordOnRegistration"]); switch ($config["operationMode"]) { case "synapse": // register with registration_shared_secret - // generate a password with 10 characters - $password = bin2hex(openssl_random_pseudo_bytes(5)); + if ($use_db_password && isset($user["password"]) && strlen($user["password"]) > 0) { + $password = $user["password"]; + } else { + $use_db_password = false; + // generate a password with 10 characters + $password = bin2hex(openssl_random_pseudo_bytes(5)); + } $res = $mxConn->register($username, $password, $config["registration_shared_secret"]); if (!$res) { // something went wrong while registering @@ -84,6 +90,7 @@ try { break; case "local": // register by adding a user to the local database + $use_db_password = false; // requires restructure to use db-provided pw $password = $mx_db->addUser($first_name, $last_name, $username, $email); break; default: @@ -92,7 +99,13 @@ try { if ($password != NULL) { // send registration_success $res = send_mail_registration_success( - $config["homeserver"], $first_name . " " . $last_name, $email, $username, $password, $config["howToURL"] + $config["homeserver"], + $first_name . " " . $last_name, + $email, + $username, + // only send password when auto-created + ($use_db_password ? NULL : $password), + $config["howToURL"] ); if ($res) { $mx_db->setRegistrationStateAdmin(RegisterState::AllDone, $token); -- 2.39.5 From 9a93b88d1154b50612126e7ea8a5e8a542ba6eff Mon Sep 17 00:00:00 2001 From: Krombel Date: Mon, 28 May 2018 10:29:41 +0200 Subject: [PATCH 2/3] allow captured password for operationMode=local as well --- cron.php | 6 ++++-- database.php | 10 ++++++---- public/verify_admin.php | 17 ++++++++--------- 3 files changed, 18 insertions(+), 15 deletions(-) diff --git a/cron.php b/cron.php index cc7b2e0..8c5aba8 100644 --- a/cron.php +++ b/cron.php @@ -19,7 +19,9 @@ require_once(__DIR__ . "/language.php"); require_once(__DIR__ . "/mail_templates.php"); require_once(__DIR__ . "/database.php"); -$sql = "SELECT id, first_name, last_name, username, email, state, note, verify_token, admin_token FROM registrations " +$sql = "SELECT id, first_name, last_name, username, password, email," + . " state, note, verify_token, admin_token " + . "FROM registrations " . "WHERE state = " . RegisterState::PendingEmailSend . " OR state = " . RegisterState::PendingAdminSend . " OR state = " . RegisterState::PendingRegistration @@ -87,7 +89,7 @@ foreach ($mx_db->query($sql) as $row) { break; case "local": // register by adding a user to the local database - $password = $mx_db->addUser($row["first_name"], $row["last_name"], $row["username"], $row["email"]); + $password = $mx_db->addUser($row["first_name"], $row["last_name"], $row["username"], $row["password"], $row["email"]); break; default: throw new Exception("Unknown operationMode"); diff --git a/database.php b/database.php index f53a5be..d42f902 100644 --- a/database.php +++ b/database.php @@ -98,7 +98,7 @@ class mxDatabase { )"); // make sure the bot is allowed to login if (!$this->userRegistered("register_bot")) { - $password = $this->addUser("Register", "Bot", "register_bot", $config["register_email"]); + $password = $this->addUser("Register", "Bot", "register_bot", NULL, $config["register_email"]); $config["register_password"] = $password; $myfile = fopen(dirname(__FILE__) . "/config.json", "w"); fwrite($myfile, json_encode($config, JSON_PRETTY_PRINT)); @@ -283,14 +283,16 @@ class mxDatabase { * NULL when failed * */ - function addUser($first_name, $last_name, $username, $email) { + function addUser($first_name, $last_name, $username, $password, $email) { // check if user already exists and abort in that case if ($this->userRegistered($username)) { return NULL; } - // generate a password with 10 characters - $password = bin2hex(openssl_random_pseudo_bytes(5)); + if ($password == NULL) { + // generate a password with 10 characters + $password = bin2hex(openssl_random_pseudo_bytes(5)); + } $password_hash = password_hash($password, PASSWORD_BCRYPT, ["cost" => 12]); $sql = "INSERT INTO logins (first_name, last_name, localpart, password_hash, email) VALUES " diff --git a/public/verify_admin.php b/public/verify_admin.php index 3028eee..e3c6b09 100644 --- a/public/verify_admin.php +++ b/public/verify_admin.php @@ -72,16 +72,16 @@ try { $password = NULL; $use_db_password = (isset($config["getPasswordOnRegistration"]) && $config["getPasswordOnRegistration"]); + if ($use_db_password && isset($user["password"]) && strlen($user["password"]) > 0) { + $password = $user["password"]; + } else { + $use_db_password = false; + // generate a password with 10 characters + $password = bin2hex(openssl_random_pseudo_bytes(5)); + } switch ($config["operationMode"]) { case "synapse": // register with registration_shared_secret - if ($use_db_password && isset($user["password"]) && strlen($user["password"]) > 0) { - $password = $user["password"]; - } else { - $use_db_password = false; - // generate a password with 10 characters - $password = bin2hex(openssl_random_pseudo_bytes(5)); - } $res = $mxConn->register($username, $password, $config["registration_shared_secret"]); if (!$res) { // something went wrong while registering @@ -90,8 +90,7 @@ try { break; case "local": // register by adding a user to the local database - $use_db_password = false; // requires restructure to use db-provided pw - $password = $mx_db->addUser($first_name, $last_name, $username, $email); + $password = $mx_db->addUser($first_name, $last_name, $username, $password, $email); break; default: throw new Exception("Unknown operationMode"); -- 2.39.5 From b4d630cd9e92a1cf7817e8d6d54487ef955d68f5 Mon Sep 17 00:00:00 2001 From: Krombel Date: Mon, 28 May 2018 10:45:31 +0200 Subject: [PATCH 3/3] add Requirements section --- README.md | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index fa59928..f1866c8 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,15 @@ When using `operationMode=local` you can have the following benefits (some requi - Use the 3PID lookup for other users (only email) - Search for users that you have not seen yet +## Requirements + +- Working PHP environment with + - database connection provider \[one of sqlite, mysql, postgres\] + - curl extension to notify admins and register users (in `operationMode=synapse`) + - mail capability to interact with the users (Verification, Approval (+ initial password), Notifications) +- matrix-synapse-rest-auth when using `operationMode=local` +- some PHP capable webserver which makes the folder `public` accessible to the public and propably `internal` for server-internal access + ## How to install - Copy `config.sample.php` to `config.php` and configure the bot as you can find there @@ -43,15 +52,11 @@ When using `operationMode=local` you can have the following benefits (some requi ## Further notes: -### This bot sends mails -To allow the bot to verify the email address of the user and to interact with them e.g. in case of approval this bot needs a running mailserver configuration. -This bot relies on php to be properly configured. - -### Security: Passwords for registration are stored in clear text +### Security: Passwords from registration form are stored in clear text Currently the passwords which are typed in while capturing the register request are stored in clear text. +The bot needs to access them to trigger a register request with correct credentials. It is currently strongly recommended to set `"getPasswordOnRegistration" => false` in your config! -This leads to autocreating passwords which will then be send to the users directly -without storing it. +This leads to autocreating passwords which will then be send to the users directly without storing it. ### Use the ChangePasswortInterceptor (if `operationMode=local`) -- 2.39.5