VYPR
Moderate severityNVD Advisory· Published Dec 11, 2023· Updated Oct 9, 2024

Uptime Kuma Password Change Vulnerability

CVE-2023-49804

Description

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.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
uptime-kumanpm
< 1.23.91.23.9

Affected products

1

Patches

1
482049c72b3a

Merge pull request from GHSA-88j4-pcx8-q4q3

https://github.com/louislam/uptime-kumaLouis LamDec 10, 2023via ghsa
16 files changed · +170 54
  • config/vite.config.js+0 2 modified
    @@ -3,7 +3,6 @@ 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");
    @@ -22,7 +21,6 @@ export default defineConfig({
             "CODESPACE_NAME": JSON.stringify(process.env.CODESPACE_NAME),
         },
         plugins: [
    -        commonjs(),
             vue(),
             legacy({
                 targets: [ "since 2015" ],
    
  • extra/healthcheck.js+1 1 modified
    @@ -6,7 +6,7 @@
      * ⚠️ 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";
     
    
  • extra/reset-password.js+45 0 modified
    @@ -5,6 +5,8 @@ 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,
    @@ -36,12 +38,16 @@ const main = async () => {
                         // 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);
    @@ -66,6 +72,45 @@ function question(question) {
         });
     }
     
    +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();
     }
    
  • package.json+0 1 modified
    @@ -192,7 +192,6 @@
             "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",
    
  • server/config.js+34 21 modified
    @@ -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,
     };
    
  • server/routers/api-router.js+1 2 modified
    @@ -11,12 +11,11 @@ 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();
    
  • server/routers/status-page-router.js+1 1 modified
    @@ -5,7 +5,7 @@ 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();
    
  • server/server.js+6 10 modified
    @@ -81,7 +81,7 @@ 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;
     
    @@ -91,7 +91,7 @@ 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");
    @@ -115,19 +115,13 @@ 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;
    @@ -1157,6 +1151,8 @@ 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.",
    
  • server/socket-handlers/general-socket-handler.js+10 0 modified
    @@ -78,4 +78,14 @@ module.exports.generalSocketHandler = (socket, server) => {
                 });
             }
         });
    +
    +    // 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);
    +        }
    +    });
     };
    
  • server/uptime-kuma-server.js+24 9 modified
    @@ -12,6 +12,7 @@ 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.
     
     /**
    @@ -62,22 +63,17 @@ class UptimeKumaServer {
          */
         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),
    @@ -422,6 +418,25 @@ class UptimeKumaServer {
                 }
             }
         }
    +
    +    /**
    +     * 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 = {
    
  • server/util-server.js+1 2 modified
    @@ -1,15 +1,14 @@
     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;
    
  • src/components/BadgeGeneratorDialog.vue+2 2 modified
    @@ -135,7 +135,7 @@
     <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: {
    @@ -230,7 +230,7 @@ export default {
                         "labelColor",
                     ],
                 },
    -            badgeConstants: serverConfig.badgeConstants,
    +            badgeConstants,
             };
         },
     
    
  • src/mixins/socket.js+4 0 modified
    @@ -288,6 +288,10 @@ export default {
                 socket.on("initServerTimezone", () => {
                     socket.emit("initServerTimezone", dayjs.tz.guess());
                 });
    +
    +            socket.on("refresh", () => {
    +                location.reload();
    +            });
             },
     
             /**
    
  • src/pages/EditMonitor.vue+1 2 modified
    @@ -848,9 +848,8 @@ 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();
    
  • src/util.js+20 1 modified
    @@ -7,7 +7,7 @@
     // 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";
    @@ -24,6 +24,25 @@ 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) {
    
  • src/util.ts+20 0 modified
    @@ -29,6 +29,26 @@ 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) {
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

5

News mentions

0

No linked articles in our index yet.