CVE-2023-49804 (JavaScript, published 2023-12-11T23:15Z)
Description: 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.
Severity: nan (CVSS v3 base score: 7.8)
Weakness: CWE-384 \u2014 Session Fixation
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.
Repository: https://github.com/louislam/uptime-kuma
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 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

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"
};