CVE-2023-49804: Session Fixation (CWE-384)
value: Uptime Kuma is an easy-to-use self-hosted monitoring tool. Prior to version 1.23.9, when a user changes their login password in Uptime Kuma, a previously logged-in user retains access without being logged out. This behavior persists consistently, even after system restarts or browser restarts. This vulnerability allows unauthorized access to user accounts, compromising the security of sensitive information. The same vulnerability was partially fixed in  CVE-2023-44400, but logging existing users out of their accounts was forgotten. To mitigate the risks associated with this vulnerability, the maintainers made the server emit a `refresh` event (clients handle this by reloading) and then disconnecting all clients except the one initiating the password change. It is recommended to update Uptime Kuma to version 1.23.9.
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-12-11T23:15Z
Repository: https://github.com/louislam/uptime-kuma
Language: JavaScript
Files changed: config/vite.config.js, extra/healthcheck.js, extra/reset-password.js, package.json, server/config.js, server/routers/api-router.js, server/routers/status-page-router.js, server/server.js, server/socket-handlers/general-socket-handler.js, server/uptime-kuma-server.js, server/util-server.js, src/components/BadgeGeneratorDialog.vue, src/mixins/socket.js, src/pages/EditMonitor.vue, src/util.js, src/util.ts
Commit date: 2023-12-10 20:40:40 +0800
Commit message: Merge pull request from GHSA-88j4-pcx8-q4q3

* WIP, still need to handle npm run reset-password

* Implement it for "npm run reset-password"

Bug fixes and change along with this commit
- Move `ssl`, `hostname`, `port` to ./server/config.js, so `reset-password` is able to read it
- Fix: FBSD is missing, no idea who dropped it.
- Fix: Frontend code should not require any backend code (./server/config.js), moved "badgeConstants" to the common util (./src/util.ts) and drop vite-common.js

* Minor
Diff: diff --git a/config/vite.config.js b/config/vite.config.js
index 11c610066..8aaa7d02c 100644
--- a/config/vite.config.js
+++ b/config/vite.config.js
@@ -1,11 +1,10 @@
 import legacy from "@vitejs/plugin-legacy";
 import vue from "@vitejs/plugin-vue";
 import { defineConfig } from "vite";
 import visualizer from "rollup-plugin-visualizer";
 import viteCompression from "vite-plugin-compression";
-import commonjs from "vite-plugin-commonjs";
 
 const postCssScss = require("postcss-scss");
 const postcssRTLCSS = require("postcss-rtlcss");
 
 const viteCompressionFilter = /\.(js|mjs|json|css|html|svg)$/i;
@@ -20,11 +19,10 @@ export default defineConfig({
         "DEVCONTAINER": JSON.stringify(process.env.DEVCONTAINER),
         "GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN": JSON.stringify(process.env.GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN),
         "CODESPACE_NAME": JSON.stringify(process.env.CODESPACE_NAME),
     },
     plugins: [
-        commonjs(),
         vue(),
         legacy({
             targets: [ "since 2015" ],
         }),
         visualizer({
diff --git a/extra/healthcheck.js b/extra/healthcheck.js
index 5e06c2120..c9391c410 100644
--- a/extra/healthcheck.js
+++ b/extra/healthcheck.js
@@ -4,11 +4,11 @@
  * See more: https://github.com/louislam/uptime-kuma/issues/2774#issuecomment-1429092359
  *
  * ⚠️ Deprecated: Changed to healthcheck.go, it will be deleted in the future.
  * This script should be run after a period of time (180s), because the server may need some time to prepare.
  */
-const { FBSD } = require("../server/util-server");
+const FBSD = /^freebsd/.test(process.platform);
 
 process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
 
 let client;
 
diff --git a/extra/reset-password.js b/extra/reset-password.js
index 168983312..7f23a83f0 100644
--- a/extra/reset-password.js
+++ b/extra/reset-password.js
@@ -3,10 +3,12 @@ console.log("== Uptime Kuma Reset Password Tool ==");
 const Database = require("../server/database");
 const { R } = require("redbean-node");
 const readline = require("readline");
 const { initJWTSecret } = require("../server/util-server");
 const User = require("../server/model/user");
+const { io } = require("socket.io-client");
+const { localWebSocketURL } = require("../server/config");
 const args = require("args-parser")(process.argv);
 const rl = readline.createInterface({
     input: process.stdin,
     output: process.stdout
 });
@@ -34,16 +36,20 @@ const main = async () => {
                     await User.resetPassword(user.id, password);
 
                     // Reset all sessions by reset jwt secret
                     await initJWTSecret();
 
+                    // Disconnect all other socket clients of the user
+                    await disconnectAllSocketClients(user.username, password);
+
                     break;
                 } else {
                     console.log("Passwords do not match, please try again.");
                 }
             }
             console.log("Password reset successfully.");
+
         }
     } catch (e) {
         console.error("Error: " + e.message);
     }
 
@@ -64,10 +70,49 @@ function question(question) {
             resolve(answer);
         });
     });
 }
 
+function disconnectAllSocketClients(username, password) {
+    return new Promise((resolve) => {
+        console.log("Connecting to " + localWebSocketURL + " to disconnect all other socket clients");
+
+        // Disconnect all socket connections
+        const socket = io(localWebSocketURL, {
+            transports: [ "websocket" ],
+            reconnection: false,
+            timeout: 5000,
+        });
+        socket.on("connect", () => {
+            socket.emit("login", {
+                username,
+                password,
+            }, (res) => {
+                if (res.ok) {
+                    console.log("Logged in.");
+                    socket.emit("disconnectOtherSocketClients");
+                } else {
+                    console.warn("Login failed.");
+                    console.warn("Please restart the server to disconnect all sessions.");
+                }
+                socket.close();
+            });
+        });
+
+        socket.on("connect_error", function () {
+            // The localWebSocketURL is not guaranteed to be working for some complicated Uptime Kuma setup
+            // Ask the user to restart the server manually
+            console.warn("Failed to connect to " + localWebSocketURL);
+            console.warn("Please restart the server to disconnect all sessions manually.");
+            resolve();
+        });
+        socket.on("disconnect", () => {
+            resolve();
+        });
+    });
+}
+
 if (!process.env.TEST_BACKEND) {
     main();
 }
 
 module.exports = {
diff --git a/package.json b/package.json
index 54b0f54b5..95d6f8783 100644
--- a/package.json
+++ b/package.json
@@ -190,11 +190,10 @@
         "terser": "~5.15.0",
         "timezones-list": "~3.0.1",
         "typescript": "~4.4.4",
         "v-pagination-3": "~0.1.7",
         "vite": "~4.4.1",
-        "vite-plugin-commonjs": "^0.8.0",
         "vite-plugin-compression": "^0.5.1",
         "vue": "~3.3.4",
         "vue-chartjs": "~5.2.0",
         "vue-confirm-dialog": "~1.0.2",
         "vue-contenteditable": "~3.0.4",
diff --git a/server/config.js b/server/config.js
index 77f9e74b3..635c37e7f 100644
--- a/server/config.js
+++ b/server/config.js
@@ -1,29 +1,42 @@
+const isFreeBSD = /^freebsd/.test(process.platform);
+
 // Interop with browser
 const args = (typeof process !== "undefined") ? require("args-parser")(process.argv) : {};
-const demoMode = args["demo"] || false;
 
-const badgeConstants = {
-    naColor: "#999",
-    defaultUpColor: "#66c20a",
-    defaultWarnColor: "#eed202",
-    defaultDownColor: "#c2290a",
-    defaultPendingColor: "#f8a306",
-    defaultMaintenanceColor: "#1747f5",
-    defaultPingColor: "blue",  // as defined by badge-maker / shields.io
-    defaultStyle: "flat",
-    defaultPingValueSuffix: "ms",
-    defaultPingLabelSuffix: "h",
-    defaultUptimeValueSuffix: "%",
-    defaultUptimeLabelSuffix: "h",
-    defaultCertExpValueSuffix: " days",
-    defaultCertExpLabelSuffix: "h",
-    // Values Come From Default Notification Times
-    defaultCertExpireWarnDays: "14",
-    defaultCertExpireDownDays: "7"
-};
+// If host is omitted, the server will accept connections on the unspecified IPv6 address (::) when IPv6 is available and the unspecified IPv4 address (0.0.0.0) otherwise.
+// Dual-stack support for (::)
+// Also read HOST if not FreeBSD, as HOST is a system environment variable in FreeBSD
+let hostEnv = isFreeBSD ? null : process.env.HOST;
+const hostname = args.host || process.env.UPTIME_KUMA_HOST || hostEnv;
+
+const port = [ args.port, process.env.UPTIME_KUMA_PORT, process.env.PORT, 3001 ]
+    .map(portValue => parseInt(portValue))
+    .find(portValue => !isNaN(portValue));
+
+const sslKey = args["ssl-key"] || process.env.UPTIME_KUMA_SSL_KEY || process.env.SSL_KEY || undefined;
+const sslCert = args["ssl-cert"] || process.env.UPTIME_KUMA_SSL_CERT || process.env.SSL_CERT || undefined;
+const sslKeyPassphrase = args["ssl-key-passphrase"] || process.env.UPTIME_KUMA_SSL_KEY_PASSPHRASE || process.env.SSL_KEY_PASSPHRASE || undefined;
+
+const isSSL = sslKey && sslCert;
+
+function getLocalWebSocketURL() {
+    const protocol = isSSL ? "wss" : "ws";
+    const host = hostname || "localhost";
+    return `${protocol}://${host}:${port}`;
+}
+
+const localWebSocketURL = getLocalWebSocketURL();
+
+const demoMode = args["demo"] || false;
 
 module.exports = {
     args,
+    hostname,
+    port,
+    sslKey,
+    sslCert,
+    sslKeyPassphrase,
+    isSSL,
+    localWebSocketURL,
     demoMode,
-    badgeConstants,
 };
diff --git a/server/routers/api-router.js b/server/routers/api-router.js
index acf8d56b1..c9b2ec884 100644
--- a/server/routers/api-router.js
+++ b/server/routers/api-router.js
@@ -9,16 +9,15 @@ const {
 } = require("../util-server");
 const { R } = require("redbean-node");
 const apicache = require("../modules/apicache");
 const Monitor = require("../model/monitor");
 const dayjs = require("dayjs");
-const { UP, MAINTENANCE, DOWN, PENDING, flipStatus, log } = require("../../src/util");
+const { UP, MAINTENANCE, DOWN, PENDING, flipStatus, log, badgeConstants } = require("../../src/util");
 const StatusPage = require("../model/status_page");
 const { UptimeKumaServer } = require("../uptime-kuma-server");
 const { UptimeCacheList } = require("../uptime-cache-list");
 const { makeBadge } = require("badge-maker");
-const { badgeConstants } = require("../config");
 const { Prometheus } = require("../prometheus");
 
 let router = express.Router();
 
 let cache = apicache.middleware;
diff --git a/server/routers/status-page-router.js b/server/routers/status-page-router.js
index b60f286ee..c7a580d87 100644
--- a/server/routers/status-page-router.js
+++ b/server/routers/status-page-router.js
@@ -3,11 +3,11 @@ const apicache = require("../modules/apicache");
 const { UptimeKumaServer } = require("../uptime-kuma-server");
 const StatusPage = require("../model/status_page");
 const { allowDevAllOrigin, sendHttpError } = require("../util-server");
 const { R } = require("redbean-node");
 const Monitor = require("../model/monitor");
-const { badgeConstants } = require("../config");
+const { badgeConstants } = require("../../src/util");
 const { makeBadge } = require("badge-maker");
 
 let router = express.Router();
 
 let cache = apicache.middleware;
diff --git a/server/server.js b/server/server.js
index 11174f769..af37c949a 100644
--- a/server/server.js
+++ b/server/server.js
@@ -79,21 +79,21 @@ const { passwordStrength } = require("check-password-strength");
 log.debug("server", "Importing 2FA Modules");
 const notp = require("notp");
 const base32 = require("thirty-two");
 
 const { UptimeKumaServer } = require("./uptime-kuma-server");
-const server = UptimeKumaServer.getInstance(args);
+const server = UptimeKumaServer.getInstance();
 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, shake256, SHAKE256_LENGTH
+const { getSettings, setSettings, setting, initJWTSecret, checkLogin, startUnitTest, doubleCheckPassword, startE2eTests, shake256, SHAKE256_LENGTH
 } = require("./util-server");
 
 log.debug("server", "Importing Notification");
 const { Notification } = require("./notification");
 Notification.init();
@@ -113,23 +113,17 @@ const { login } = require("./auth");
 const passwordHash = require("./password-hash");
 
 const checkVersion = require("./check-version");
 log.info("server", "Version: " + checkVersion.version);
 
-// If host is omitted, the server will accept connections on the unspecified IPv6 address (::) when IPv6 is available and the unspecified IPv4 address (0.0.0.0) otherwise.
-// Dual-stack support for (::)
-// Also read HOST if not FreeBSD, as HOST is a system environment variable in FreeBSD
-let hostEnv = FBSD ? null : process.env.HOST;
-let hostname = args.host || process.env.UPTIME_KUMA_HOST || hostEnv;
+const hostname = config.hostname;
 
 if (hostname) {
     log.info("server", "Custom hostname: " + hostname);
 }
 
-const port = [ args.port, process.env.UPTIME_KUMA_PORT, process.env.PORT, 3001 ]
-    .map(portValue => parseInt(portValue))
-    .find(portValue => !isNaN(portValue));
+const port = config.port;
 
 const disableFrameSameOrigin = !!process.env.UPTIME_KUMA_DISABLE_FRAME_SAMEORIGIN || args["disable-frame-sameorigin"] || false;
 const cloudflaredToken = args["cloudflared-token"] || process.env.UPTIME_KUMA_CLOUDFLARED_TOKEN || undefined;
 
 // 2FA / notp verification defaults
@@ -1155,10 +1149,12 @@ let needSetup = false;
                 }
 
                 let user = await doubleCheckPassword(socket, password.currentPassword);
                 await user.resetPassword(password.newPassword);
 
+                server.disconnectAllSocketClient(user.id, socket.id);
+
                 callback({
                     ok: true,
                     msg: "Password has been updated successfully.",
                 });
 
diff --git a/server/socket-handlers/general-socket-handler.js b/server/socket-handlers/general-socket-handler.js
index 3fc6f1d57..b3163c1c1 100644
--- a/server/socket-handlers/general-socket-handler.js
+++ b/server/socket-handlers/general-socket-handler.js
@@ -76,6 +76,16 @@ module.exports.generalSocketHandler = (socket, server) => {
                 ok: false,
                 msg: e.message,
             });
         }
     });
+
+    // Disconnect all other socket clients of the user
+    socket.on("disconnectOtherSocketClients", async () => {
+        try {
+            checkLogin(socket);
+            server.disconnectAllSocketClients(socket.userID, socket.id);
+        } catch (e) {
+            log.warn("disconnectAllSocketClients", e.message);
+        }
+    });
 };
diff --git a/server/uptime-kuma-server.js b/server/uptime-kuma-server.js
index 07dfc30f6..556f19d55 100644
--- a/server/uptime-kuma-server.js
+++ b/server/uptime-kuma-server.js
@@ -10,10 +10,11 @@ const util = require("util");
 const { CacheableDnsHttpAgent } = require("./cacheable-dns-http-agent");
 const { Settings } = require("./settings");
 const dayjs = require("dayjs");
 const childProcessAsync = require("promisify-child-process");
 const path = require("path");
+const { isSSL, sslKey, sslCert, sslKeyPassphrase } = require("./config");
 // DO NOT IMPORT HERE IF THE MODULES USED `UptimeKumaServer.getInstance()`, put at the bottom of this file instead.
 
 /**
  * `module.exports` (alias: `server`) should be inside this class, in order to avoid circular dependency issue.
  * @type {UptimeKumaServer}
@@ -60,26 +61,21 @@ class UptimeKumaServer {
      * Use for decode the auth object
      * @type {null}
      */
     jwtSecret = null;
 
-    static getInstance(args) {
+    static getInstance() {
         if (UptimeKumaServer.instance == null) {
-            UptimeKumaServer.instance = new UptimeKumaServer(args);
+            UptimeKumaServer.instance = new UptimeKumaServer();
         }
         return UptimeKumaServer.instance;
     }
 
-    constructor(args) {
-        // SSL
-        const sslKey = args["ssl-key"] || process.env.UPTIME_KUMA_SSL_KEY || process.env.SSL_KEY || undefined;
-        const sslCert = args["ssl-cert"] || process.env.UPTIME_KUMA_SSL_CERT || process.env.SSL_CERT || undefined;
-        const sslKeyPassphrase = args["ssl-key-passphrase"] || process.env.UPTIME_KUMA_SSL_KEY_PASSPHRASE || process.env.SSL_KEY_PASSPHRASE || undefined;
-
+    constructor() {
         log.info("server", "Creating express and socket.io instance");
         this.app = express();
-        if (sslKey && sslCert) {
+        if (isSSL) {
             log.info("server", "Server Type: HTTPS");
             this.httpServer = https.createServer({
                 key: fs.readFileSync(sslKey),
                 cert: fs.readFileSync(sslCert),
                 passphrase: sslKeyPassphrase,
@@ -420,10 +416,29 @@ class UptimeKumaServer {
             } catch (e) {
                 log.info("services", "Failed to stop nscd");
             }
         }
     }
+
+    /**
+     * Force connected sockets of a user to refresh and disconnect.
+     * Used for resetting password.
+     * @param {string} userID
+     * @param {string?} currentSocketID
+     */
+    disconnectAllSocketClients(userID, currentSocketID = undefined) {
+        for (const socket of this.io.sockets.sockets.values()) {
+            if (socket.userID === userID && socket.id !== currentSocketID) {
+                try {
+                    socket.emit("refresh");
+                    socket.disconnect();
+                } catch (e) {
+
+                }
+            }
+        }
+    }
 }
 
 module.exports = {
     UptimeKumaServer
 };
diff --git a/server/util-server.js b/server/util-server.js
index ec0cb7684..705888806 100644
--- a/server/util-server.js
+++ b/server/util-server.js
@@ -1,17 +1,16 @@
 const tcpp = require("tcp-ping");
 const ping = require("@louislam/ping");
 const { R } = require("redbean-node");
-const { log, genSecret } = require("../src/util");
+const { log, genSecret, badgeConstants } = require("../src/util");
 const passwordHash = require("./password-hash");
 const { Resolver } = require("dns");
 const childProcess = require("child_process");
 const iconv = require("iconv-lite");
 const chardet = require("chardet");
 const mqtt = require("mqtt");
 const chroma = require("chroma-js");
-const { badgeConstants } = require("./config");
 const mssql = require("mssql");
 const { Client } = require("pg");
 const postgresConParse = require("pg-connection-string").parse;
 const mysql = require("mysql2");
 const { MongoClient } = require("mongodb");
diff --git a/src/components/BadgeGeneratorDialog.vue b/src/components/BadgeGeneratorDialog.vue
index aa6fa6e88..252a18c5b 100644
--- a/src/components/BadgeGeneratorDialog.vue
+++ b/src/components/BadgeGeneratorDialog.vue
@@ -133,11 +133,11 @@
 </template>
 
 <script lang="ts">
 import { Modal } from "bootstrap";
 import CopyableInput from "./CopyableInput.vue";
-import { default as serverConfig } from "../../server/config.js";
+import { badgeConstants } from "../util.ts";
 
 export default {
     components: {
         CopyableInput
     },
@@ -228,11 +228,11 @@ export default {
                     "suffix",
                     "color",
                     "labelColor",
                 ],
             },
-            badgeConstants: serverConfig.badgeConstants,
+            badgeConstants,
         };
     },
 
     computed: {
         badgeURL() {
diff --git a/src/mixins/socket.js b/src/mixins/socket.js
index fabd34406..41c7fabd3 100644
--- a/src/mixins/socket.js
+++ b/src/mixins/socket.js
@@ -286,10 +286,14 @@ export default {
             socket.on("cloudflared_token", (res) => this.cloudflared.cloudflareTunnelToken = res);
 
             socket.on("initServerTimezone", () => {
                 socket.emit("initServerTimezone", dayjs.tz.guess());
             });
+
+            socket.on("refresh", () => {
+                location.reload();
+            });
         },
 
         /**
          * The storage currently in use
          * @returns {Storage}
diff --git a/src/pages/EditMonitor.vue b/src/pages/EditMonitor.vue
index 92454a267..83ec58ac8 100644
--- a/src/pages/EditMonitor.vue
+++ b/src/pages/EditMonitor.vue
@@ -846,13 +846,12 @@ import CopyableInput from "../components/CopyableInput.vue";
 import CreateGroupDialog from "../components/CreateGroupDialog.vue";
 import NotificationDialog from "../components/NotificationDialog.vue";
 import DockerHostDialog from "../components/DockerHostDialog.vue";
 import ProxyDialog from "../components/ProxyDialog.vue";
 import TagsManager from "../components/TagsManager.vue";
-import { genSecret, isDev, MAX_INTERVAL_SECOND, MIN_INTERVAL_SECOND } from "../util.ts";
+import { genSecret, isDev, MAX_INTERVAL_SECOND, MIN_INTERVAL_SECOND, sleep } from "../util.ts";
 import { hostNameRegexPattern } from "../util-frontend";
-import { sleep } from "../util";
 import HiddenInput from "../components/HiddenInput.vue";
 
 const toast = useToast();
 
 const monitorDefaults = {
diff --git a/src/util.js b/src/util.js
index 8936ffbf9..f37be1fdf 100644
--- a/src/util.js
+++ b/src/util.js
@@ -5,11 +5,11 @@
 // Need to run "tsc" to compile if there are any changes.
 //
 // Backend uses the compiled file util.js
 // Frontend uses util.ts
 Object.defineProperty(exports, "__esModule", { value: true });
-exports.localToUTC = exports.utcToLocal = exports.utcToISODateTime = exports.isoToUTCDateTime = exports.parseTimeFromTimeObject = exports.parseTimeObject = exports.getMaintenanceRelativeURL = exports.getMonitorRelativeURL = exports.genSecret = exports.getCryptoRandomInt = exports.getRandomInt = exports.getRandomArbitrary = exports.TimeLogger = exports.polyfill = exports.log = exports.debug = exports.ucfirst = exports.sleep = exports.flipStatus = exports.MIN_INTERVAL_SECOND = exports.MAX_INTERVAL_SECOND = exports.SQL_DATETIME_FORMAT_WITHOUT_SECOND = exports.SQL_DATETIME_FORMAT = exports.SQL_DATE_FORMAT = exports.STATUS_PAGE_MAINTENANCE = exports.STATUS_PAGE_PARTIAL_DOWN = exports.STATUS_PAGE_ALL_UP = exports.STATUS_PAGE_ALL_DOWN = exports.MAINTENANCE = exports.PENDING = exports.UP = exports.DOWN = exports.appName = exports.isDev = void 0;
+exports.localToUTC = exports.utcToLocal = exports.utcToISODateTime = exports.isoToUTCDateTime = exports.parseTimeFromTimeObject = exports.parseTimeObject = exports.getMaintenanceRelativeURL = exports.getMonitorRelativeURL = exports.genSecret = exports.getCryptoRandomInt = exports.getRandomInt = exports.getRandomArbitrary = exports.TimeLogger = exports.polyfill = exports.log = exports.debug = exports.ucfirst = exports.sleep = exports.flipStatus = exports.badgeConstants = exports.MIN_INTERVAL_SECOND = exports.MAX_INTERVAL_SECOND = exports.SQL_DATETIME_FORMAT_WITHOUT_SECOND = exports.SQL_DATETIME_FORMAT = exports.SQL_DATE_FORMAT = exports.STATUS_PAGE_MAINTENANCE = exports.STATUS_PAGE_PARTIAL_DOWN = exports.STATUS_PAGE_ALL_UP = exports.STATUS_PAGE_ALL_DOWN = exports.MAINTENANCE = exports.PENDING = exports.UP = exports.DOWN = exports.appName = exports.isDev = void 0;
 const dayjs = require("dayjs");
 exports.isDev = process.env.NODE_ENV === "development";
 exports.appName = "Uptime Kuma";
 exports.DOWN = 0;
 exports.UP = 1;
@@ -22,10 +22,29 @@ exports.STATUS_PAGE_MAINTENANCE = 3;
 exports.SQL_DATE_FORMAT = "YYYY-MM-DD";
 exports.SQL_DATETIME_FORMAT = "YYYY-MM-DD HH:mm:ss";
 exports.SQL_DATETIME_FORMAT_WITHOUT_SECOND = "YYYY-MM-DD HH:mm";
 exports.MAX_INTERVAL_SECOND = 2073600; // 24 days
 exports.MIN_INTERVAL_SECOND = 20; // 20 seconds
+exports.badgeConstants = {
+    naColor: "#999",
+    defaultUpColor: "#66c20a",
+    defaultWarnColor: "#eed202",
+    defaultDownColor: "#c2290a",
+    defaultPendingColor: "#f8a306",
+    defaultMaintenanceColor: "#1747f5",
+    defaultPingColor: "blue",
+    defaultStyle: "flat",
+    defaultPingValueSuffix: "ms",
+    defaultPingLabelSuffix: "h",
+    defaultUptimeValueSuffix: "%",
+    defaultUptimeLabelSuffix: "h",
+    defaultCertExpValueSuffix: " days",
+    defaultCertExpLabelSuffix: "h",
+    // Values Come From Default Notification Times
+    defaultCertExpireWarnDays: "14",
+    defaultCertExpireDownDays: "7"
+};
 /** Flip the status of s */
 function flipStatus(s) {
     if (s === exports.UP) {
         return exports.DOWN;
     }
diff --git a/src/util.ts b/src/util.ts
index 4f62fe763..213b3701d 100644
--- a/src/util.ts
+++ b/src/util.ts
@@ -27,10 +27,30 @@ export const SQL_DATETIME_FORMAT = "YYYY-MM-DD HH:mm:ss";
 export const SQL_DATETIME_FORMAT_WITHOUT_SECOND = "YYYY-MM-DD HH:mm";
 
 export const MAX_INTERVAL_SECOND = 2073600; // 24 days
 export const MIN_INTERVAL_SECOND = 20; // 20 seconds
 
+export const badgeConstants = {
+    naColor: "#999",
+    defaultUpColor: "#66c20a",
+    defaultWarnColor: "#eed202",
+    defaultDownColor: "#c2290a",
+    defaultPendingColor: "#f8a306",
+    defaultMaintenanceColor: "#1747f5",
+    defaultPingColor: "blue",  // as defined by badge-maker / shields.io
+    defaultStyle: "flat",
+    defaultPingValueSuffix: "ms",
+    defaultPingLabelSuffix: "h",
+    defaultUptimeValueSuffix: "%",
+    defaultUptimeLabelSuffix: "h",
+    defaultCertExpValueSuffix: " days",
+    defaultCertExpLabelSuffix: "h",
+    // Values Come From Default Notification Times
+    defaultCertExpireWarnDays: "14",
+    defaultCertExpireDownDays: "7"
+};
+
 /** Flip the status of s */
 export function flipStatus(s: number) {
     if (s === UP) {
         return DOWN;
     }

Vulnerable code: import commonjs from "vite-plugin-commonjs";
        commonjs(),
const { FBSD } = require("../server/util-server");
        "vite-plugin-commonjs": "^0.8.0",
const demoMode = args["demo"] || false;
const badgeConstants = {
    naColor: "#999",
    defaultUpColor: "#66c20a",
    defaultWarnColor: "#eed202",
    defaultDownColor: "#c2290a",
    defaultPendingColor: "#f8a306",
    defaultMaintenanceColor: "#1747f5",
    defaultPingColor: "blue",  // as defined by badge-maker / shields.io
    defaultStyle: "flat",
    defaultPingValueSuffix: "ms",
    defaultPingLabelSuffix: "h",
    defaultUptimeValueSuffix: "%",
    defaultUptimeLabelSuffix: "h",
    defaultCertExpValueSuffix: " days",
    defaultCertExpLabelSuffix: "h",
    // Values Come From Default Notification Times
    defaultCertExpireWarnDays: "14",
    defaultCertExpireDownDays: "7"
};
    badgeConstants,
const { UP, MAINTENANCE, DOWN, PENDING, flipStatus, log } = require("../../src/util");
const { badgeConstants } = require("../config");
const { badgeConstants } = require("../config");
const server = UptimeKumaServer.getInstance(args);
const { getSettings, setSettings, setting, initJWTSecret, checkLogin, startUnitTest, FBSD, doubleCheckPassword, startE2eTests, shake256, SHAKE256_LENGTH
// If host is omitted, the server will accept connections on the unspecified IPv6 address (::) when IPv6 is available and the unspecified IPv4 address (0.0.0.0) otherwise.
// Dual-stack support for (::)
// Also read HOST if not FreeBSD, as HOST is a system environment variable in FreeBSD
let hostEnv = FBSD ? null : process.env.HOST;
let hostname = args.host || process.env.UPTIME_KUMA_HOST || hostEnv;
const port = [ args.port, process.env.UPTIME_KUMA_PORT, process.env.PORT, 3001 ]
    .map(portValue => parseInt(portValue))
    .find(portValue => !isNaN(portValue));
    static getInstance(args) {
            UptimeKumaServer.instance = new UptimeKumaServer(args);
    constructor(args) {
        // SSL
        const sslKey = args["ssl-key"] || process.env.UPTIME_KUMA_SSL_KEY || process.env.SSL_KEY || undefined;
        const sslCert = args["ssl-cert"] || process.env.UPTIME_KUMA_SSL_CERT || process.env.SSL_CERT || undefined;
        const sslKeyPassphrase = args["ssl-key-passphrase"] || process.env.UPTIME_KUMA_SSL_KEY_PASSPHRASE || process.env.SSL_KEY_PASSPHRASE || undefined;

        if (sslKey && sslCert) {
const { log, genSecret } = require("../src/util");
const { badgeConstants } = require("./config");
import { default as serverConfig } from "../../server/config.js";
            badgeConstants: serverConfig.badgeConstants,
import { genSecret, isDev, MAX_INTERVAL_SECOND, MIN_INTERVAL_SECOND } from "../util.ts";
import { sleep } from "../util";
exports.localToUTC = exports.utcToLocal = exports.utcToISODateTime = exports.isoToUTCDateTime = exports.parseTimeFromTimeObject = exports.parseTimeObject = exports.getMaintenanceRelativeURL = exports.getMonitorRelativeURL = exports.genSecret = exports.getCryptoRandomInt = exports.getRandomInt = exports.getRandomArbitrary = exports.TimeLogger = exports.polyfill = exports.log = exports.debug = exports.ucfirst = exports.sleep = exports.flipStatus = exports.MIN_INTERVAL_SECOND = exports.MAX_INTERVAL_SECOND = exports.SQL_DATETIME_FORMAT_WITHOUT_SECOND = exports.SQL_DATETIME_FORMAT = exports.SQL_DATE_FORMAT = exports.STATUS_PAGE_MAINTENANCE = exports.STATUS_PAGE_PARTIAL_DOWN = exports.STATUS_PAGE_ALL_UP = exports.STATUS_PAGE_ALL_DOWN = exports.MAINTENANCE = exports.PENDING = exports.UP = exports.DOWN = exports.appName = exports.isDev = void 0;
Fixed code: const FBSD = /^freebsd/.test(process.platform);
const { io } = require("socket.io-client");
const { localWebSocketURL } = require("../server/config");
                    // Disconnect all other socket clients of the user
                    await disconnectAllSocketClients(user.username, password);


function disconnectAllSocketClients(username, password) {
    return new Promise((resolve) => {
        console.log("Connecting to " + localWebSocketURL + " to disconnect all other socket clients");

        // Disconnect all socket connections
        const socket = io(localWebSocketURL, {
            transports: [ "websocket" ],
            reconnection: false,
            timeout: 5000,
        });
        socket.on("connect", () => {
            socket.emit("login", {
                username,
                password,
            }, (res) => {
                if (res.ok) {
                    console.log("Logged in.");
                    socket.emit("disconnectOtherSocketClients");
                } else {
                    console.warn("Login failed.");
                    console.warn("Please restart the server to disconnect all sessions.");
                }
                socket.close();
            });
        });

        socket.on("connect_error", function () {
            // The localWebSocketURL is not guaranteed to be working for some complicated Uptime Kuma setup
            // Ask the user to restart the server manually
            console.warn("Failed to connect to " + localWebSocketURL);
            console.warn("Please restart the server to disconnect all sessions manually.");
            resolve();
        });
        socket.on("disconnect", () => {
            resolve();
        });
    });
}

const isFreeBSD = /^freebsd/.test(process.platform);

// If host is omitted, the server will accept connections on the unspecified IPv6 address (::) when IPv6 is available and the unspecified IPv4 address (0.0.0.0) otherwise.
// Dual-stack support for (::)
// Also read HOST if not FreeBSD, as HOST is a system environment variable in FreeBSD
let hostEnv = isFreeBSD ? null : process.env.HOST;
const hostname = args.host || process.env.UPTIME_KUMA_HOST || hostEnv;

const port = [ args.port, process.env.UPTIME_KUMA_PORT, process.env.PORT, 3001 ]
    .map(portValue => parseInt(portValue))
    .find(portValue => !isNaN(portValue));

const sslKey = args["ssl-key"] || process.env.UPTIME_KUMA_SSL_KEY || process.env.SSL_KEY || undefined;
const sslCert = args["ssl-cert"] || process.env.UPTIME_KUMA_SSL_CERT || process.env.SSL_CERT || undefined;
const sslKeyPassphrase = args["ssl-key-passphrase"] || process.env.UPTIME_KUMA_SSL_KEY_PASSPHRASE || process.env.SSL_KEY_PASSPHRASE || undefined;

const isSSL = sslKey && sslCert;

function getLocalWebSocketURL() {
    const protocol = isSSL ? "wss" : "ws";
    const host = hostname || "localhost";
    return `${protocol}://${host}:${port}`;
}

const localWebSocketURL = getLocalWebSocketURL();

const demoMode = args["demo"] || false;
    hostname,
    port,
    sslKey,
    sslCert,
    sslKeyPassphrase,
    isSSL,
    localWebSocketURL,
const { UP, MAINTENANCE, DOWN, PENDING, flipStatus, log, badgeConstants } = require("../../src/util");
const { badgeConstants } = require("../../src/util");
const server = UptimeKumaServer.getInstance();
const { getSettings, setSettings, setting, initJWTSecret, checkLogin, startUnitTest, doubleCheckPassword, startE2eTests, shake256, SHAKE256_LENGTH
const hostname = config.hostname;
const port = config.port;
                server.disconnectAllSocketClient(user.id, socket.id);


    // Disconnect all other socket clients of the user
    socket.on("disconnectOtherSocketClients", async () => {
        try {
            checkLogin(socket);
            server.disconnectAllSocketClients(socket.userID, socket.id);
        } catch (e) {
            log.warn("disconnectAllSocketClients", e.message);
        }
    });
const { isSSL, sslKey, sslCert, sslKeyPassphrase } = require("./config");
    static getInstance() {
            UptimeKumaServer.instance = new UptimeKumaServer();
    constructor() {
        if (isSSL) {

    /**
     * Force connected sockets of a user to refresh and disconnect.
     * Used for resetting password.
     * @param {string} userID
     * @param {string?} currentSocketID
     */
    disconnectAllSocketClients(userID, currentSocketID = undefined) {
        for (const socket of this.io.sockets.sockets.values()) {
            if (socket.userID === userID && socket.id !== currentSocketID) {
                try {
                    socket.emit("refresh");
                    socket.disconnect();
                } catch (e) {

                }
            }
        }
    }
const { log, genSecret, badgeConstants } = require("../src/util");
import { badgeConstants } from "../util.ts";
            badgeConstants,

            socket.on("refresh", () => {
                location.reload();
            });
import { genSecret, isDev, MAX_INTERVAL_SECOND, MIN_INTERVAL_SECOND, sleep } from "../util.ts";
exports.localToUTC = exports.utcToLocal = exports.utcToISODateTime = exports.isoToUTCDateTime = exports.parseTimeFromTimeObject = exports.parseTimeObject = exports.getMaintenanceRelativeURL = exports.getMonitorRelativeURL = exports.genSecret = exports.getCryptoRandomInt = exports.getRandomInt = exports.getRandomArbitrary = exports.TimeLogger = exports.polyfill = exports.log = exports.debug = exports.ucfirst = exports.sleep = exports.flipStatus = exports.badgeConstants = exports.MIN_INTERVAL_SECOND = exports.MAX_INTERVAL_SECOND = exports.SQL_DATETIME_FORMAT_WITHOUT_SECOND = exports.SQL_DATETIME_FORMAT = exports.SQL_DATE_FORMAT = exports.STATUS_PAGE_MAINTENANCE = exports.STATUS_PAGE_PARTIAL_DOWN = exports.STATUS_PAGE_ALL_UP = exports.STATUS_PAGE_ALL_DOWN = exports.MAINTENANCE = exports.PENDING = exports.UP = exports.DOWN = exports.appName = exports.isDev = void 0;
exports.badgeConstants = {
    naColor: "#999",
    defaultUpColor: "#66c20a",
    defaultWarnColor: "#eed202",
    defaultDownColor: "#c2290a",
    defaultPendingColor: "#f8a306",
    defaultMaintenanceColor: "#1747f5",
    defaultPingColor: "blue",
    defaultStyle: "flat",
    defaultPingValueSuffix: "ms",
    defaultPingLabelSuffix: "h",
    defaultUptimeValueSuffix: "%",
    defaultUptimeLabelSuffix: "h",
    defaultCertExpValueSuffix: " days",
    defaultCertExpLabelSuffix: "h",
    // Values Come From Default Notification Times
    defaultCertExpireWarnDays: "14",
    defaultCertExpireDownDays: "7"
};
export const badgeConstants = {
    naColor: "#999",
    defaultUpColor: "#66c20a",
    defaultWarnColor: "#eed202",
    defaultDownColor: "#c2290a",
    defaultPendingColor: "#f8a306",
    defaultMaintenanceColor: "#1747f5",
    defaultPingColor: "blue",  // as defined by badge-maker / shields.io
    defaultStyle: "flat",
    defaultPingValueSuffix: "ms",
    defaultPingLabelSuffix: "h",
    defaultUptimeValueSuffix: "%",
    defaultUptimeLabelSuffix: "h",
    defaultCertExpValueSuffix: " days",
    defaultCertExpLabelSuffix: "h",
    // Values Come From Default Notification Times
    defaultCertExpireWarnDays: "14",
    defaultCertExpireDownDays: "7"
};