CVE-2022-2820 (published 2022-08-15T11:21Z)

Session Fixation in GitHub repository namelessmc/nameless prior to v2.0.2.\n\n

CVSS v3 base score: 8.2, HIGH (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:L/A:N)

Weakness information:
cwe_id: CWE-384
cwe_name: Session Fixation
description: Authenticating a user, or otherwise establishing a new user session, without invalidating any existing session identifier gives an attacker the opportunity to steal authenticated sessions.
extended_description: 
            Such a scenario is commonly observed when:
            
               A web application authenticates a user without first invalidating the existing session, thereby continuing to use the session already associated with the user.
               An attacker is able to force a known session identifier on a user so that, once the user authenticates, the attacker has access to the authenticated session.
               The application or container uses predictable session identifiers.
	    
	    In the generic exploit of session fixation vulnerabilities, an attacker creates a new session on a web application and records the associated session identifier. The attacker then causes the victim to associate, and possibly authenticate, against the server using that session identifier, giving the attacker access to the user's account through the active session.
         
         

Fix information:
repo_url: https://github.com/namelessmc/nameless
commit_details: repo_url: https://github.com/namelessmc/nameless
author: Partydragen
author_date: 2022-08-13 06:29:09+02:00
msg: Rework user sessions system (#3000)

Co-authored-by: samerton <samerton@users.noreply.github.com>
repository: repo_url: https://github.com/namelessmc/nameless
repo_name: NamelessMC/Nameless
description: NamelessMC is a free, easy to use & powerful website software for your Minecraft server, which includes a large range of features.
repo_language: PHP
owner: namelessmc
file_changes: filename: User.php
change_type: ModificationType.MODIFY
diff: @@ -6,7 +6,7 @@
  * @author Samerton
  * @author Partydragen
  * @author Aberdeener
- * @version 2.0.0-pr13
+ * @version 2.0.2
  * @license MIT
  */
 class User {
@@ -73,14 +73,14 @@ class User {
 
         if ($user === null) {
             if (Session::exists($this->_sessionName)) {
-                $user = Session::get($this->_sessionName);
-                if ($this->find($user, $field)) {
+                $hash = Session::get($this->_sessionName);
+                if ($this->find($hash, 'hash')) {
                     $this->_isLoggedIn = true;
                 }
             }
             if (Session::exists($this->_admSessionName)) {
-                $user = Session::get($this->_admSessionName);
-                if ($user == $this->data()->id && $this->find($user, $field)) {
+                $hash = Session::get($this->_admSessionName);
+                if ($this->find($hash, 'hash')) {
                     $this->_isAdmLoggedIn = true;
                 }
             }
@@ -107,7 +107,11 @@ class User {
                 return true;
             }
 
-            $data = $this->_db->get('users', [$field, $value]);
+            if ($field != 'hash') {
+                $data = $this->_db->get('users', [$field, $value]);
+            } else {
+                $data = $this->_db->query('SELECT nl2_users.* FROM nl2_users LEFT JOIN nl2_users_session ON nl2_users.id = user_id WHERE hash = ? AND nl2_users_session.active = 1', [$value]);
+            }
 
             if ($data->count()) {
                 $this->_data = new UserData($data->first());
@@ -297,7 +301,7 @@ class User {
      * @param string|null $username Their username (or email, depending on $method).
      * @param string|null $password Their password.
      * @param bool $remember Whether to keep them logged in or not.
-     * @param string $method What column to check for their details in. Can be either `username` or `email`.
+     * @param string $method What column to check for their details in. Can be either `username` or `email` or `oauth`.
      *
      * @return bool True/false on success or failure respectfully.
      */
@@ -307,29 +311,27 @@ class User {
 
     private function _commonLogin(?string $username, ?string $password, bool $remember, string $method, bool $is_admin): bool {
         $sessionName = $is_admin ? $this->_admSessionName : $this->_sessionName;
-        if (!$username && !$password && $this->exists()) {
-            Session::put($sessionName, $this->data()->id);
+        if (!$username && $method == 'hash' && $this->exists()) {
+            // Logged in using hash from cookie
+            Session::put($sessionName, $password);
             if (!$is_admin) {
                 $this->_isLoggedIn = true;
             }
         } else if ($this->checkCredentials($username, $password, $method) === true) {
             // Valid credentials
-            Session::put($sessionName, $this->data()->id);
+            $hash = SecureRandom::alphanumeric();
+
+            $this->_db->insert('users_session', [
+                'user_id' => $this->data()->id,
+                'hash' => $hash,
+                'remember_me' => $remember,
+                'active' => 1,
+                'login_method' => $is_admin ? 'admin' : $method
+            ]);
 
-            if ($remember) {
-                $hash = SecureRandom::alphanumeric();
-                $table = $is_admin ? 'users_admin_session' : 'users_session';
-                $hashCheck = $this->_db->get($table, ['user_id', $this->data()->id]);
-
-                if (!$hashCheck->count()) {
-                    $this->_db->insert($table, [
-                        'user_id' => $this->data()->id,
-                        'hash' => $hash
-                    ]);
-                } else {
-                    $hash = $hashCheck->first()->hash;
-                }
+            Session::put($sessionName, $hash);
 
+            if ($remember) {
                 $expiry = $is_admin ? 3600 : Config::get('remember.cookie_expiry');
                 $cookieName = $is_admin ? ($this->_cookieName . '_adm') : $this->_cookieName;
                 Cookie::put($cookieName, $hash, $expiry, HttpUtils::getProtocol() === 'https', true);
@@ -532,12 +534,24 @@ class User {
         return false;
     }
 
+    /**
+     * Log the user out from all other sessions.
+     */
+    public function logoutAllOtherSessions(): void {
+        DB::getInstance()->query('UPDATE nl2_users_session SET `active` = 0 WHERE user_id = ? AND hash != ?', [
+            $this->data()->id,
+            Session::get(Config::get('session.session_name'))
+        ]);
+    }
+
     /**
      * Log the user out.
      * Deletes their cookies, sessions and database session entry.
      */
     public function logout(): void {
-        $this->_db->delete('users_session', ['user_id', $this->data()->id]);
+        $this->_db->update('users_session', [['user_id', $this->data()->id], ['hash', Session::get($this->_sessionName)]], [
+            'active' => 0
+        ]);
 
         Session::delete($this->_sessionName);
         Cookie::delete($this->_cookieName);
@@ -547,7 +561,9 @@ class User {
      * Process logout if user is admin
      */
     public function admLogout(): void {
-        $this->_db->delete('users_admin_session', ['user_id', $this->data()->id]);
+        $this->_db->update('users_session', [['user_id', $this->data()->id], ['hash', Session::get($this->_admSessionName)]], [
+            'active' => 0
+        ]);
 
         Session::delete($this->_admSessionName);
         Cookie::delete($this->_cookieName . '_adm');

programming_language: PHP
filename: init.php
change_type: ModificationType.MODIFY
diff: @@ -2,7 +2,7 @@
 /*
  *  Made by Samerton
  *  https://github.com/NamelessMC/Nameless/
- *  NamelessMC version 2.0.0-pr9
+ *  NamelessMC version 2.0.2
  *
  *  License: MIT
  *
@@ -132,11 +132,11 @@ if ($page != 'install') {
     // Do they need logging in (checked remember me)?
     if (Cookie::exists(Config::get('remember.cookie_name')) && !Session::exists(Config::get('session.session_name'))) {
         $hash = Cookie::get(Config::get('remember.cookie_name'));
-        $hashCheck = DB::getInstance()->get('users_session', ['hash', $hash]);
+        $hashCheck = DB::getInstance()->get('users_session', [['hash', $hash], ['active', true]]);
 
         if ($hashCheck->count()) {
             $user = new User($hashCheck->first()->user_id);
-            $user->login();
+            $user->login(null, $hash, true, 'hash');
         }
     }
 

programming_language: PHP
filename: upgrade_perform.php
change_type: ModificationType.MODIFY
diff: @@ -521,43 +521,6 @@ try {
     $errors[] = 'Unable to convert users: ' . $e->getMessage();
 }
 
-// User admin session -> user profile wall replies
-// User admin sessions
-try {
-    $old = $conn->get('nl1_users_admin_session', ['id', '<>', 0]);
-    if ($old->count()) {
-        $old = $old->results();
-
-        foreach ($old as $item) {
-            DB::getInstance()->insert('users_admin_session', [
-                'id' => $item->id,
-                'user_id' => $item->user_id,
-                'hash' => $item->hash
-            ]);
-        }
-    }
-} catch (Exception $e) {
-    $errors[] = 'Unable to convert user admin sessions: ' . $e->getMessage();
-}
-
-// User sessions
-try {
-    $old = $conn->get('nl1_users_session', ['id', '<>', 0]);
-    if ($old->count()) {
-        $old = $old->results();
-
-        foreach ($old as $item) {
-            DB::getInstance()->insert('users_session', [
-                'id' => $item->id,
-                'user_id' => $item->user_id,
-                'hash' => $item->hash
-            ]);
-        }
-    }
-} catch (Exception $e) {
-    $errors[] = 'Unable to convert user sessions: ' . $e->getMessage();
-}
-
 // Username history
 try {
     $old = $conn->get('nl1_users_username_history', ['id', '<>', 0]);

programming_language: PHP
filename: 20220807142446_add_user_session_activity_columns.php
change_type: ModificationType.ADD
diff: @@ -0,0 +1,22 @@
+<?php
+declare(strict_types=1);
+
+use Phinx\Migration\AbstractMigration;
+
+final class AddUserSessionActivityColumns extends AbstractMigration
+{
+    public function change(): void
+    {
+        $table = $this->table('nl2_users_session');
+        $table->addColumn('remember_me', 'boolean', ['default' => false]);
+        $table->addColumn('active', 'boolean', ['default' => false]);
+        $table->addColumn('device_name', 'string', ['length' => 256, 'null' => true, 'default' => null]);
+        $table->addColumn('last_seen', 'integer', ['length' => 11, 'null' => true, 'default' => null]);
+        $table->addColumn('login_method', 'string', ['length' => 32]);
+        $table->addIndex('hash', ['unique' => true]);
+        $table->update();
+
+        // Remove old data
+        $table->truncate();
+    }
+}

programming_language: PHP
filename: 20220807153708_delete_admin_session_table.php
change_type: ModificationType.ADD
diff: @@ -0,0 +1,14 @@
+<?php
+declare(strict_types=1);
+
+use Phinx\Migration\AbstractMigration;
+
+final class DeleteAdminSessionTable extends AbstractMigration
+{
+    public function change(): void
+    {
+        $table = $this->table('nl2_users_admin_session');
+        $table->drop();
+        $table->update();
+    }
+}

programming_language: PHP
filename: DeleteUserHook.php
change_type: ModificationType.MODIFY
diff: @@ -54,7 +54,6 @@ class DeleteUserHook {
             $db->delete('reports_comments', ['commenter_id', $params['user_id']]);
 
             // User sessions
-            $db->delete('users_admin_session', ['user_id', $params['user_id']]);
             $db->delete('users_session', ['user_id', $params['user_id']]);
 
             // Profile fields

programming_language: PHP
filename: settings.php
change_type: ModificationType.MODIFY
diff: @@ -2,7 +2,7 @@
 /*
  *  Made by Samerton
  *  https://github.com/NamelessMC/Nameless/
- *  NamelessMC version 2.0.0-pr13
+ *  NamelessMC version 2.0.2
  *
  *  License: MIT
  *
@@ -80,6 +80,10 @@ if (isset($_GET['do'])) {
                                 'tfa_enabled' => true,
                                 'tfa_type' => 1
                             ]);
+
+                            // Logout all other sessions for this user
+                            $user->logoutAllOtherSessions();
+
                             Session::delete('force_tfa_alert');
                             Session::flash('tfa_success', $language->get('user', 'tfa_successful'));
                             Redirect::to(URL::build('/user/settings'));
@@ -364,6 +368,9 @@ if (isset($_GET['do'])) {
                                 'pass_method' => 'default'
                             ]);
 
+                            // Logout all other sessions for this user
+                            $user->logoutAllOtherSessions();
+
                             $success = $language->get('user', 'password_changed_successfully');
 
                         } else {
@@ -410,7 +417,7 @@ if (isset($_GET['do'])) {
 
                                     // Update email
                                     $user->update([
-                                        'email' => Output::getClean($_POST['email'])
+                                        'email' => $_POST['email']
                                     ]);
 
                                     Session::flash('settings_success', $language->get('user', 'email_changed_successfully'));

programming_language: PHP