VYPR
Moderate severityNVD Advisory· Published Jul 5, 2023· Updated Oct 24, 2024

Uptime Kuma authenticated path traversal via plugin repository name may lead to unavailability or data loss

CVE-2023-36822

Description

Uptime Kuma, a self-hosted monitoring tool, has a path traversal vulnerability in versions prior to 1.22.1. Uptime Kuma allows authenticated users to install plugins from an official list of plugins. This feature is currently disabled in the web interface, but the corresponding API endpoints are still available after login. Before a plugin is downloaded, the plugin installation directory is checked for existence. If it exists, it's removed before the plugin installation. Because the plugin is not validated against the official list of plugins or sanitized, the check for existence and the removal of the plugin installation directory are prone to path traversal. This vulnerability allows an authenticated attacker to delete files from the server Uptime Kuma is running on. Depending on which files are deleted, Uptime Kuma or the whole system may become unavailable due to data loss.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
uptime-kumanpm
< 1.22.11.22.1

Affected products

1

Patches

1
a0736e04b283

Merge pull request #3346 from louislam/revert-plugin-code

https://github.com/louislam/uptime-kumaLouis LamJul 3, 2023via ghsa
12 files changed · +15 594
  • package-lock.json+14 5 modified
    @@ -133,6 +133,15 @@
                     "node": "14.* || 16.* || 18.*"
                 }
             },
    +        "node_modules/@aashutoshrathi/word-wrap": {
    +            "version": "1.2.6",
    +            "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz",
    +            "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==",
    +            "dev": true,
    +            "engines": {
    +                "node": ">=0.10.0"
    +            }
    +        },
             "node_modules/@actions/github": {
                 "version": "5.0.3",
                 "resolved": "https://registry.npmjs.org/@actions/github/-/github-5.0.3.tgz",
    @@ -15193,17 +15202,17 @@
                 }
             },
             "node_modules/optionator": {
    -            "version": "0.9.1",
    -            "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz",
    -            "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==",
    +            "version": "0.9.3",
    +            "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz",
    +            "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==",
                 "dev": true,
                 "dependencies": {
    +                "@aashutoshrathi/word-wrap": "^1.2.3",
                     "deep-is": "^0.1.3",
                     "fast-levenshtein": "^2.0.6",
                     "levn": "^0.4.1",
                     "prelude-ls": "^1.2.1",
    -                "type-check": "^0.4.0",
    -                "word-wrap": "^1.2.3"
    +                "type-check": "^0.4.0"
                 },
                 "engines": {
                     "node": ">= 0.8.0"
    
  • server/database.js+1 7 modified
    @@ -3,7 +3,7 @@ const { R } = require("redbean-node");
     const { setSetting, setting } = require("./util-server");
     const { log, sleep } = require("../src/util");
     const knex = require("knex");
    -const { PluginsManager } = require("./plugins-manager");
    +
     
     /**
      * Database & App Data Folder
    @@ -88,12 +88,6 @@ class Database {
             // Data Directory (must be end with "/")
             Database.dataDir = process.env.DATA_DIR || args["data-dir"] || "./data/";
     
    -        // Plugin feature is working only if the dataDir = "./data";
    -        if (Database.dataDir !== "./data/") {
    -            log.warn("PLUGIN", "Warning: In order to enable plugin feature, you need to use the default data directory: ./data/");
    -            PluginsManager.disable = true;
    -        }
    -
             Database.path = Database.dataDir + "kuma.db";
             if (! fs.existsSync(Database.dataDir)) {
                 fs.mkdirSync(Database.dataDir, { recursive: true });
    
  • server/git.js+0 24 removed
    @@ -1,24 +0,0 @@
    -const childProcess = require("child_process");
    -
    -class Git {
    -
    -    static clone(repoURL, cwd, targetDir = ".") {
    -        let result = childProcess.spawnSync("git", [
    -            "clone",
    -            repoURL,
    -            targetDir,
    -        ], {
    -            cwd: cwd,
    -        });
    -
    -        if (result.status !== 0) {
    -            throw new Error(result.stderr.toString("utf-8"));
    -        } else {
    -            return result.stdout.toString("utf-8") + result.stderr.toString("utf-8");
    -        }
    -    }
    -}
    -
    -module.exports = {
    -    Git,
    -};
    
  • server/plugin.js+0 13 removed
    @@ -1,13 +0,0 @@
    -class Plugin {
    -    async load() {
    -
    -    }
    -
    -    async unload() {
    -
    -    }
    -}
    -
    -module.exports = {
    -    Plugin,
    -};
    
  • server/plugins-manager.js+0 256 removed
    @@ -1,256 +0,0 @@
    -const fs = require("fs");
    -const { log } = require("../src/util");
    -const path = require("path");
    -const axios = require("axios");
    -const { Git } = require("./git");
    -const childProcess = require("child_process");
    -
    -class PluginsManager {
    -
    -    static disable = false;
    -
    -    /**
    -     * Plugin List
    -     * @type {PluginWrapper[]}
    -     */
    -    pluginList = [];
    -
    -    /**
    -     * Plugins Dir
    -     */
    -    pluginsDir;
    -
    -    server;
    -
    -    /**
    -     *
    -     * @param {UptimeKumaServer} server
    -     */
    -    constructor(server) {
    -        this.server = server;
    -
    -        if (!PluginsManager.disable) {
    -            this.pluginsDir = "./data/plugins/";
    -
    -            if (! fs.existsSync(this.pluginsDir)) {
    -                fs.mkdirSync(this.pluginsDir, { recursive: true });
    -            }
    -
    -            log.debug("plugin", "Scanning plugin directory");
    -            let list = fs.readdirSync(this.pluginsDir);
    -
    -            this.pluginList = [];
    -            for (let item of list) {
    -                this.loadPlugin(item);
    -            }
    -
    -        } else {
    -            log.warn("PLUGIN", "Skip scanning plugin directory");
    -        }
    -
    -    }
    -
    -    /**
    -     * Install a Plugin
    -     */
    -    async loadPlugin(name) {
    -        log.info("plugin", "Load " + name);
    -        let plugin = new PluginWrapper(this.server, this.pluginsDir + name);
    -
    -        try {
    -            await plugin.load();
    -            this.pluginList.push(plugin);
    -        } catch (e) {
    -            log.error("plugin", "Failed to load plugin: " + this.pluginsDir + name);
    -            log.error("plugin", "Reason: " + e.message);
    -        }
    -    }
    -
    -    /**
    -     * Download a Plugin
    -     * @param {string} repoURL Git repo url
    -     * @param {string} name Directory name, also known as plugin unique name
    -     */
    -    downloadPlugin(repoURL, name) {
    -        if (fs.existsSync(this.pluginsDir + name)) {
    -            log.info("plugin", "Plugin folder already exists? Removing...");
    -            fs.rmSync(this.pluginsDir + name, {
    -                recursive: true
    -            });
    -        }
    -        log.info("plugin", "Installing plugin: " + name + " " + repoURL);
    -        let result = Git.clone(repoURL, this.pluginsDir, name);
    -        log.info("plugin", "Install result: " + result);
    -    }
    -
    -    /**
    -     * Remove a plugin
    -     * @param {string} name
    -     */
    -    async removePlugin(name) {
    -        log.info("plugin", "Removing plugin: " + name);
    -        for (let plugin of this.pluginList) {
    -            if (plugin.info.name === name) {
    -                await plugin.unload();
    -
    -                // Delete the plugin directory
    -                fs.rmSync(this.pluginsDir + name, {
    -                    recursive: true
    -                });
    -
    -                this.pluginList.splice(this.pluginList.indexOf(plugin), 1);
    -                return;
    -            }
    -        }
    -        log.warn("plugin", "Plugin not found: " + name);
    -        throw new Error("Plugin not found: " + name);
    -    }
    -
    -    /**
    -     * TODO: Update a plugin
    -     * Only available for plugins which were downloaded from the official list
    -     * @param pluginID
    -     */
    -    updatePlugin(pluginID) {
    -
    -    }
    -
    -    /**
    -     * Get the plugin list from server + local installed plugin list
    -     * Item will be merged if the `name` is the same.
    -     * @returns {Promise<[]>}
    -     */
    -    async fetchPluginList() {
    -        let remotePluginList;
    -        try {
    -            const res = await axios.get("https://uptime.kuma.pet/c/plugins.json");
    -            remotePluginList = res.data.pluginList;
    -        } catch (e) {
    -            log.error("plugin", "Failed to fetch plugin list: " + e.message);
    -            remotePluginList = [];
    -        }
    -
    -        for (let plugin of this.pluginList) {
    -            let find = false;
    -            // Try to merge
    -            for (let remotePlugin of remotePluginList) {
    -                if (remotePlugin.name === plugin.info.name) {
    -                    find = true;
    -                    remotePlugin.installed = true;
    -                    remotePlugin.name = plugin.info.name;
    -                    remotePlugin.fullName = plugin.info.fullName;
    -                    remotePlugin.description = plugin.info.description;
    -                    remotePlugin.version = plugin.info.version;
    -                    break;
    -                }
    -            }
    -
    -            // Local plugin
    -            if (!find) {
    -                plugin.info.local = true;
    -                remotePluginList.push(plugin.info);
    -            }
    -        }
    -
    -        // Sort Installed first, then sort by name
    -        return remotePluginList.sort((a, b) => {
    -            if (a.installed === b.installed) {
    -                if (a.fullName < b.fullName) {
    -                    return -1;
    -                }
    -                if (a.fullName > b.fullName) {
    -                    return 1;
    -                }
    -                return 0;
    -            } else if (a.installed) {
    -                return -1;
    -            } else {
    -                return 1;
    -            }
    -        });
    -    }
    -}
    -
    -class PluginWrapper {
    -
    -    server = undefined;
    -    pluginDir = undefined;
    -
    -    /**
    -     * Must be an `new-able` class.
    -     * @type {function}
    -     */
    -    pluginClass = undefined;
    -
    -    /**
    -     *
    -     * @type {Plugin}
    -     */
    -    object = undefined;
    -    info = {};
    -
    -    /**
    -     *
    -     * @param {UptimeKumaServer} server
    -     * @param {string} pluginDir
    -     */
    -    constructor(server, pluginDir) {
    -        this.server = server;
    -        this.pluginDir = pluginDir;
    -    }
    -
    -    async load() {
    -        let indexFile = this.pluginDir + "/index.js";
    -        let packageJSON = this.pluginDir + "/package.json";
    -
    -        log.info("plugin", "Installing dependencies");
    -
    -        if (fs.existsSync(indexFile)) {
    -            // Install dependencies
    -            let result = childProcess.spawnSync("npm", [ "install" ], {
    -                cwd: this.pluginDir,
    -                env: {
    -                    ...process.env,
    -                    PLAYWRIGHT_BROWSERS_PATH: "../../browsers",    // Special handling for read-browser-monitor
    -                }
    -            });
    -
    -            if (result.stdout) {
    -                log.info("plugin", "Install dependencies result: " + result.stdout.toString("utf-8"));
    -            } else {
    -                log.warn("plugin", "Install dependencies result: no output");
    -            }
    -
    -            this.pluginClass = require(path.join(process.cwd(), indexFile));
    -
    -            let pluginClassType = typeof this.pluginClass;
    -
    -            if (pluginClassType === "function") {
    -                this.object = new this.pluginClass(this.server);
    -                await this.object.load();
    -            } else {
    -                throw new Error("Invalid plugin, it does not export a class");
    -            }
    -
    -            if (fs.existsSync(packageJSON)) {
    -                this.info = require(path.join(process.cwd(), packageJSON));
    -            } else {
    -                this.info.fullName = this.pluginDir;
    -                this.info.name = "[unknown]";
    -                this.info.version = "[unknown-version]";
    -            }
    -
    -            this.info.installed = true;
    -            log.info("plugin", `${this.info.fullName} v${this.info.version} loaded`);
    -        }
    -    }
    -
    -    async unload() {
    -        await this.object.unload();
    -    }
    -}
    -
    -module.exports = {
    -    PluginsManager,
    -    PluginWrapper
    -};
    
  • server/server.js+0 3 modified
    @@ -147,7 +147,6 @@ const { apiKeySocketHandler } = require("./socket-handlers/api-key-socket-handle
     const { generalSocketHandler } = require("./socket-handlers/general-socket-handler");
     const { Settings } = require("./settings");
     const { CacheableDnsHttpAgent } = require("./cacheable-dns-http-agent");
    -const { pluginsHandler } = require("./socket-handlers/plugins-handler");
     const apicache = require("./modules/apicache");
     
     app.use(express.json());
    @@ -177,7 +176,6 @@ let needSetup = false;
         Database.init(args);
         await initDatabase(testMode);
         await server.initAfterDatabaseReady();
    -    server.loadPlugins();
         server.entryPage = await Settings.get("entryPage");
         await StatusPage.loadDomainMappingList();
     
    @@ -1537,7 +1535,6 @@ let needSetup = false;
             maintenanceSocketHandler(socket);
             apiKeySocketHandler(socket);
             generalSocketHandler(socket, server);
    -        pluginsHandler(socket, server);
     
             log.debug("server", "added all socket handlers");
     
    
  • server/socket-handlers/plugins-handler.js+0 69 removed
    @@ -1,69 +0,0 @@
    -const { checkLogin } = require("../util-server");
    -const { PluginsManager } = require("../plugins-manager");
    -const { log } = require("../../src/util.js");
    -
    -/**
    - * Handlers for plugins
    - * @param {Socket} socket Socket.io instance
    - * @param {UptimeKumaServer} server
    - */
    -module.exports.pluginsHandler = (socket, server) => {
    -
    -    const pluginManager = server.getPluginManager();
    -
    -    // Get Plugin List
    -    socket.on("getPluginList", async (callback) => {
    -        try {
    -            checkLogin(socket);
    -
    -            log.debug("plugin", "PluginManager.disable: " + PluginsManager.disable);
    -
    -            if (PluginsManager.disable) {
    -                throw new Error("Plugin Disabled: In order to enable plugin feature, you need to use the default data directory: ./data/");
    -            }
    -
    -            let pluginList = await pluginManager.fetchPluginList();
    -            callback({
    -                ok: true,
    -                pluginList,
    -            });
    -        } catch (error) {
    -            log.warn("plugin", "Error: " + error.message);
    -            callback({
    -                ok: false,
    -                msg: error.message,
    -            });
    -        }
    -    });
    -
    -    socket.on("installPlugin", async (repoURL, name, callback) => {
    -        try {
    -            checkLogin(socket);
    -            pluginManager.downloadPlugin(repoURL, name);
    -            await pluginManager.loadPlugin(name);
    -            callback({
    -                ok: true,
    -            });
    -        } catch (error) {
    -            callback({
    -                ok: false,
    -                msg: error.message,
    -            });
    -        }
    -    });
    -
    -    socket.on("uninstallPlugin", async (name, callback) => {
    -        try {
    -            checkLogin(socket);
    -            await pluginManager.removePlugin(name);
    -            callback({
    -                ok: true,
    -            });
    -        } catch (error) {
    -            callback({
    -                ok: false,
    -                msg: error.message,
    -            });
    -        }
    -    });
    -};
    
  • server/uptime-kuma-server.js+0 47 modified
    @@ -10,7 +10,6 @@ const util = require("util");
     const { CacheableDnsHttpAgent } = require("./cacheable-dns-http-agent");
     const { Settings } = require("./settings");
     const dayjs = require("dayjs");
    -const { PluginsManager } = require("./plugins-manager");
     // DO NOT IMPORT HERE IF THE MODULES USED `UptimeKumaServer.getInstance()`
     
     /**
    @@ -47,12 +46,6 @@ class UptimeKumaServer {
          */
         indexHTML = "";
     
    -    /**
    -     * Plugins Manager
    -     * @type {PluginsManager}
    -     */
    -    pluginsManager = null;
    -
         /**
          *
          * @type {{}}
    @@ -289,46 +282,6 @@ class UptimeKumaServer {
         async stop() {
     
         }
    -
    -    loadPlugins() {
    -        this.pluginsManager = new PluginsManager(this);
    -    }
    -
    -    /**
    -     *
    -     * @returns {PluginsManager}
    -     */
    -    getPluginManager() {
    -        return this.pluginsManager;
    -    }
    -
    -    /**
    -     *
    -     * @param {MonitorType} monitorType
    -     */
    -    addMonitorType(monitorType) {
    -        if (monitorType instanceof MonitorType && monitorType.name) {
    -            if (monitorType.name in UptimeKumaServer.monitorTypeList) {
    -                log.error("", "Conflict Monitor Type name");
    -            }
    -            UptimeKumaServer.monitorTypeList[monitorType.name] = monitorType;
    -        } else {
    -            log.error("", "Invalid Monitor Type: " + monitorType.name);
    -        }
    -    }
    -
    -    /**
    -     *
    -     * @param {MonitorType} monitorType
    -     */
    -    removeMonitorType(monitorType) {
    -        if (UptimeKumaServer.monitorTypeList[monitorType.name] === monitorType) {
    -            delete UptimeKumaServer.monitorTypeList[monitorType.name];
    -        } else {
    -            log.error("", "Remove MonitorType failed: " + monitorType.name);
    -        }
    -    }
    -
     }
     
     module.exports = {
    
  • src/components/PluginItem.vue+0 102 removed
    @@ -1,102 +0,0 @@
    -<template>
    -    <div v-if="! (!plugin.installed && plugin.local)" class="plugin-item pt-4 pb-2">
    -        <div class="info">
    -            <h5>{{ plugin.fullName }}</h5>
    -            <p class="description">
    -                {{ plugin.description }}
    -            </p>
    -            <span class="version">{{ $t("Version") }}: {{ plugin.version }} <a v-if="plugin.repo" :href="plugin.repo" target="_blank">Repo</a></span>
    -        </div>
    -        <div class="buttons">
    -            <button v-if="status === 'installing'" class="btn btn-primary" disabled>{{ $t("installing") }}</button>
    -            <button v-else-if="status === 'uninstalling'" class="btn btn-danger" disabled>{{ $t("uninstalling") }}</button>
    -            <button v-else-if="plugin.installed || status === 'installed'" class="btn btn-danger" @click="deleteConfirm">{{ $t("uninstall") }}</button>
    -            <button v-else class="btn btn-primary" @click="install">{{ $t("install") }}</button>
    -        </div>
    -
    -        <Confirm ref="confirmDelete" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="uninstall">
    -            {{ $t("confirmUninstallPlugin") }}
    -        </Confirm>
    -    </div>
    -</template>
    -
    -<script>
    -import Confirm from "./Confirm.vue";
    -
    -export default {
    -    components: {
    -        Confirm,
    -    },
    -    props: {
    -        plugin: {
    -            type: Object,
    -            required: true,
    -        },
    -    },
    -    data() {
    -        return {
    -            status: "",
    -        };
    -    },
    -    methods: {
    -        /**
    -         * Show confirmation for deleting a tag
    -         */
    -        deleteConfirm() {
    -            this.$refs.confirmDelete.show();
    -        },
    -
    -        install() {
    -            this.status = "installing";
    -
    -            this.$root.getSocket().emit("installPlugin", this.plugin.repo, this.plugin.name, (res) => {
    -                if (res.ok) {
    -                    this.status = "";
    -                    // eslint-disable-next-line vue/no-mutating-props
    -                    this.plugin.installed = true;
    -                } else {
    -                    this.$root.toastRes(res);
    -                }
    -            });
    -        },
    -
    -        uninstall() {
    -            this.status = "uninstalling";
    -
    -            this.$root.getSocket().emit("uninstallPlugin", this.plugin.name, (res) => {
    -                if (res.ok) {
    -                    this.status = "";
    -                    // eslint-disable-next-line vue/no-mutating-props
    -                    this.plugin.installed = false;
    -                } else {
    -                    this.$root.toastRes(res);
    -                }
    -            });
    -        }
    -    }
    -};
    -</script>
    -
    -<style lang="scss" scoped>
    -@import "../assets/vars.scss";
    -
    -.plugin-item {
    -    display: flex;
    -    justify-content: space-between;
    -    align-content: center;
    -    align-items: center;
    -
    -    .info {
    -        margin-right: 10px;
    -    }
    -
    -    .description {
    -        font-size: 13px;
    -        margin-bottom: 0;
    -    }
    -
    -    .version {
    -        font-size: 13px;
    -    }
    -}
    -</style>
    
  • src/components/settings/Plugins.vue+0 57 removed
    @@ -1,57 +0,0 @@
    -<template>
    -    <div>
    -        <div class="mt-3">{{ remotePluginListMsg }}</div>
    -        <PluginItem v-for="plugin in remotePluginList" :key="plugin.id" :plugin="plugin" />
    -    </div>
    -</template>
    -
    -<script>
    -import PluginItem from "../PluginItem.vue";
    -
    -export default {
    -    components: {
    -        PluginItem
    -    },
    -
    -    data() {
    -        return {
    -            remotePluginList: [],
    -            remotePluginListMsg: "",
    -        };
    -    },
    -
    -    computed: {
    -        pluginList() {
    -            return this.$parent.$parent.$parent.pluginList;
    -        },
    -        settings() {
    -            return this.$parent.$parent.$parent.settings;
    -        },
    -        saveSettings() {
    -            return this.$parent.$parent.$parent.saveSettings;
    -        },
    -        settingsLoaded() {
    -            return this.$parent.$parent.$parent.settingsLoaded;
    -        },
    -    },
    -
    -    async mounted() {
    -        this.loadList();
    -    },
    -
    -    methods: {
    -        loadList() {
    -            this.remotePluginListMsg = this.$t("Loading") + "...";
    -
    -            this.$root.getSocket().emit("getPluginList", (res) => {
    -                if (res.ok) {
    -                    this.remotePluginList = res.pluginList;
    -                    this.remotePluginListMsg = "";
    -                } else {
    -                    this.remotePluginListMsg = this.$t("loadingError") + " " + res.msg;
    -                }
    -            });
    -        }
    -    },
    -};
    -</script>
    
  • src/pages/Settings.vue+0 6 modified
    @@ -116,12 +116,6 @@ export default {
                     backup: {
                         title: this.$t("Backup"),
                     },
    -                /*
    -                Hidden for now: Unfortunately, after some test, I found that Playwright requires a lot of libraries to be installed on the Linux host in order to start Chrome or Firefox.
    -                It will be hard to install, so I hide this feature for now. But it still accessible via URL: /settings/plugins.
    -                plugins: {
    -                    title: this.$tc("plugin", 2),
    -                },*/
                     about: {
                         title: this.$t("About"),
                     },
    
  • src/router.js+0 5 modified
    @@ -19,7 +19,6 @@ import DockerHosts from "./components/settings/Docker.vue";
     import MaintenanceDetails from "./pages/MaintenanceDetails.vue";
     import ManageMaintenance from "./pages/ManageMaintenance.vue";
     import APIKeys from "./components/settings/APIKeys.vue";
    -import Plugins from "./components/settings/Plugins.vue";
     
     // Settings - Sub Pages
     import Appearance from "./components/settings/Appearance.vue";
    @@ -130,10 +129,6 @@ const routes = [
                                     path: "backup",
                                     component: Backup,
                                 },
    -                            {
    -                                path: "plugins",
    -                                component: Plugins,
    -                            },
                                 {
                                     path: "about",
                                     component: About,
    

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

7

News mentions

0

No linked articles in our index yet.