CVE-2023-44400: Session Fixation (CWE-384)
value: Uptime Kuma is a self-hosted monitoring tool. Prior to version 1.23.3, attackers with access to a user's device can gain persistent account access. This is caused by missing verification of Session Tokens after password changes and/or elapsed inactivity periods. Version 1.23.3 has a patch for the issue.
CWE 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.
CVSS v3 base score: 7.8
Published: 2023-10-09T16:15Z
Repository: https://github.com/louislam/uptime-kuma
Language: JavaScript
Files changed: server/model/user.js, server/server.js, server/util-server.js
Commit date: 2023-10-09 07:01:54 +0800
Commit message: Merge pull request from GHSA-g9v2-wqcj-j99g

* Fix attempt

* Update message
Diff: diff --git a/server/model/user.js b/server/model/user.js
index fc619c74..32a5a961 100644
--- a/server/model/user.js
+++ b/server/model/user.js
@@ -1,8 +1,10 @@
 const { BeanModel } = require("redbean-node/dist/bean-model");
 const passwordHash = require("../password-hash");
 const { R } = require("redbean-node");
+const jwt = require("jsonwebtoken");
+const { shake256, SHAKE256_LENGTH } = require("../util-server");
 
 class User extends BeanModel {
     /**
      * Reset user password
      * Fix #1510, as in the context reset-password.js, there is no auto model mapping. Call this static function instead.
@@ -25,8 +27,21 @@ class User extends BeanModel {
     async resetPassword(newPassword) {
         await User.resetPassword(this.id, newPassword);
         this.password = newPassword;
     }
 
+    /**
+     * Create a new JWT for a user
+     * @param {User} user
+     * @param {string} jwtSecret
+     * @return {string}
+     */
+    static createJWT(user, jwtSecret) {
+        return jwt.sign({
+            username: user.username,
+            h: shake256(user.password, SHAKE256_LENGTH),
+        }, jwtSecret);
+    }
+
 }
 
 module.exports = User;
diff --git a/server/server.js b/server/server.js
index 305077f5..c8f826b0 100644
--- a/server/server.js
+++ b/server/server.js
@@ -81,12 +81,15 @@ const io = module.exports.io = server.io;
 const app = server.app;
 
 log.info("server", "Importing this project modules");
 log.debug("server", "Importing Monitor");
 const Monitor = require("./model/monitor");
+const User = require("./model/user");
+
 log.debug("server", "Importing Settings");
-const { getSettings, setSettings, setting, initJWTSecret, checkLogin, startUnitTest, FBSD, doubleCheckPassword, startE2eTests } = require("./util-server");
+const { getSettings, setSettings, setting, initJWTSecret, checkLogin, startUnitTest, FBSD, doubleCheckPassword, startE2eTests, shake256, SHAKE256_LENGTH
+} = require("./util-server");
 
 log.debug("server", "Importing Notification");
 const { Notification } = require("./notification");
 Notification.init();
 
@@ -294,10 +297,15 @@ let needSetup = false;
 
                 let user = await R.findOne("user", " username = ? AND active = 1 ", [
                     decoded.username,
                 ]);
 
+                // Check if the password changed
+                if (decoded.h !== shake256(user.password, SHAKE256_LENGTH)) {
+                    throw new Error("The token is invalid due to password change or old token");
+                }
+
                 if (user) {
                     log.debug("auth", "afterLogin");
                     afterLogin(socket, user);
                     log.debug("auth", "afterLogin ok");
 
@@ -314,13 +322,14 @@ let needSetup = false;
                         ok: false,
                         msg: "The user is inactive or deleted.",
                     });
                 }
             } catch (error) {
-
                 log.error("auth", `Invalid token. IP=${clientIP}`);
-
+                if (error.message) {
+                    log.error("auth", error.message, `IP=${clientIP}`);
+                }
                 callback({
                     ok: false,
                     msg: "Invalid token.",
                 });
             }
@@ -355,13 +364,11 @@ let needSetup = false;
 
                     log.info("auth", `Successfully logged in user ${data.username}. IP=${clientIP}`);
 
                     callback({
                         ok: true,
-                        token: jwt.sign({
-                            username: data.username,
-                        }, server.jwtSecret),
+                        token: User.createJWT(user, server.jwtSecret),
                     });
                 }
 
                 if (user.twofa_status === 1 && !data.token) {
 
@@ -385,13 +392,11 @@ let needSetup = false;
 
                         log.info("auth", `Successfully logged in user ${data.username}. IP=${clientIP}`);
 
                         callback({
                             ok: true,
-                            token: jwt.sign({
-                                username: data.username,
-                            }, server.jwtSecret),
+                            token: User.createJWT(user, server.jwtSecret),
                         });
                     } else {
 
                         log.warn("auth", `Invalid token provided for user ${data.username}. IP=${clientIP}`);
 
diff --git a/server/util-server.js b/server/util-server.js
index fe61e10a..0cb63ad0 100644
--- a/server/util-server.js
+++ b/server/util-server.js
@@ -31,10 +31,11 @@ const {
 const dayjs = require("dayjs");
 
 // SASLOptions used in JSDoc
 // eslint-disable-next-line no-unused-vars
 const { Kafka, SASLOptions } = require("kafkajs");
+const crypto = require("crypto");
 
 const isWindows = process.platform === /^win/.test(process.platform);
 /**
  * Init or reset JWT secret
  * @returns {Promise<Bean>}
@@ -1053,10 +1054,27 @@ module.exports.grpcQuery = async (options) => {
         }
 
     });
 };
 
+module.exports.SHAKE256_LENGTH = 16;
+
+/**
+ *
+ * @param {string} data
+ * @param {number} len
+ * @return {string}
+ */
+module.exports.shake256 = (data, len) => {
+    if (!data) {
+        return "";
+    }
+    return crypto.createHash("shake256", { outputLength: len })
+        .update(data)
+        .digest("hex");
+};
+
 // For unit test, export functions
 if (process.env.TEST_BACKEND) {
     module.exports.__test = {
         parseCertificateInfo,
     };

Vulnerable code: const { getSettings, setSettings, setting, initJWTSecret, checkLogin, startUnitTest, FBSD, doubleCheckPassword, startE2eTests } = require("./util-server");


                        token: jwt.sign({
                            username: data.username,
                        }, server.jwtSecret),
                            token: jwt.sign({
                                username: data.username,
                            }, server.jwtSecret),
Fixed code: const jwt = require("jsonwebtoken");
const { shake256, SHAKE256_LENGTH } = require("../util-server");
    /**
     * Create a new JWT for a user
     * @param {User} user
     * @param {string} jwtSecret
     * @return {string}
     */
    static createJWT(user, jwtSecret) {
        return jwt.sign({
            username: user.username,
            h: shake256(user.password, SHAKE256_LENGTH),
        }, jwtSecret);
    }

const User = require("./model/user");

const { getSettings, setSettings, setting, initJWTSecret, checkLogin, startUnitTest, FBSD, doubleCheckPassword, startE2eTests, shake256, SHAKE256_LENGTH
} = require("./util-server");
                // Check if the password changed
                if (decoded.h !== shake256(user.password, SHAKE256_LENGTH)) {
                    throw new Error("The token is invalid due to password change or old token");
                }

                if (error.message) {
                    log.error("auth", error.message, `IP=${clientIP}`);
                }
                        token: User.createJWT(user, server.jwtSecret),
                            token: User.createJWT(user, server.jwtSecret),
const crypto = require("crypto");
module.exports.SHAKE256_LENGTH = 16;

/**
 *
 * @param {string} data
 * @param {number} len
 * @return {string}
 */
module.exports.shake256 = (data, len) => {
    if (!data) {
        return "";
    }
    return crypto.createHash("shake256", { outputLength: len })
        .update(data)
        .digest("hex");
};