CVE-2023-44400 (published 2023-10-09T16:15Z)

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.

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

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/louislam/uptime-kuma
commit_details: repo_url: https://github.com/louislam/uptime-kuma
author: Louis Lam
author_date: 2023-10-09 07:01:54+08:00
msg: Merge pull request from GHSA-g9v2-wqcj-j99g

* Fix attempt

* Update message
repository: repo_url: https://github.com/louislam/uptime-kuma
repo_name: louislam/uptime-kuma
description: A fancy self-hosted monitoring tool
repo_language: JavaScript
owner: louislam
file_changes: filename: user.js
change_type: ModificationType.MODIFY
diff: @@ -1,6 +1,8 @@
 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 {
     /**
@@ -27,6 +29,19 @@ class User extends BeanModel {
         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;

programming_language: JavaScript
filename: server.js
change_type: ModificationType.MODIFY
diff: @@ -83,8 +83,11 @@ 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");
@@ -296,6 +299,11 @@ let needSetup = false;
                     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);
@@ -316,9 +324,10 @@ let needSetup = false;
                     });
                 }
             } 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.",
@@ -357,9 +366,7 @@ let needSetup = false;
 
                     callback({
                         ok: true,
-                        token: jwt.sign({
-                            username: data.username,
-                        }, server.jwtSecret),
+                        token: User.createJWT(user, server.jwtSecret),
                     });
                 }
 
@@ -387,9 +394,7 @@ let needSetup = false;
 
                         callback({
                             ok: true,
-                            token: jwt.sign({
-                                username: data.username,
-                            }, server.jwtSecret),
+                            token: User.createJWT(user, server.jwtSecret),
                         });
                     } else {
 

programming_language: JavaScript
filename: util-server.js
change_type: ModificationType.MODIFY
diff: @@ -33,6 +33,7 @@ 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);
 /**
@@ -1055,6 +1056,23 @@ 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 = {

programming_language: TypeScript