CVE-2023-49805 (published 2023-12-11T23:15Z)

Uptime Kuma is an easy-to-use self-hosted monitoring tool. Prior to version 1.23.9, the application uses WebSocket (with Socket.io), but it does not verify that the source of communication is valid. This allows third-party website to access the application on behalf of their client. When connecting to the server using Socket.IO, the server does not validate the `Origin` header leading to other site being able to open connections to the server and communicate with it. Other websites still need to authenticate to access most features, however this can be used to circumvent firewall protections made in place by people deploying the application.\n\nWithout origin validation, Javascript executed from another origin would be allowed to connect to the application without any user interaction. Without login credentials, such a connection is unable to access protected endpoints containing sensitive data of the application. However, such a connection may allow attacker to further exploit unseen vulnerabilities of the application. Users with "No-auth" mode configured who are relying on a reverse proxy or firewall to provide protection to the application would be especially vulnerable as it would grant the attacker full access to the application.\n\nIn version 1.23.9, additional verification of the HTTP Origin header has been added to the socket.io connection handler. By default, if the `Origin` header is present, it would be checked against the Host header. Connection would be denied if the hostnames do not match, which would indicate that the request is cross-origin. Connection would be allowed if the `Origin` header is not present. Users can override this behavior by setting environment variable `UPTIME_KUMA_WS_ORIGIN_CHECK=bypass`.

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

Weakness information:
cwe_id: CWE-346
cwe_name: Origin Validation Error
description: The product does not properly verify that the source of data or communication is valid.
extended_description: 
            
            
            
         
         

Fix information:
repo_url: https://github.com/louislam/uptime-kuma
commit_details: repo_url: https://github.com/louislam/uptime-kuma
author: Louis Lam
author_date: 2023-12-10 20:39:43+08:00
msg: Merge pull request from GHSA-mj22-23ff-2hrr

* WIP

* WIP

* Handle parsing error

* Fix matching origin issue
repository: repo_url: https://github.com/louislam/uptime-kuma
repo_name: louislam/uptime-kuma
description: A fancy self-hosted monitoring tool
repo_language: JavaScript
owner: louislam
file_changes: filename: server.js
change_type: ModificationType.MODIFY
diff: @@ -48,8 +48,13 @@ if (! process.env.NODE_ENV) {
     process.env.NODE_ENV = "production";
 }
 
+if (!process.env.UPTIME_KUMA_WS_ORIGIN_CHECK) {
+    process.env.UPTIME_KUMA_WS_ORIGIN_CHECK = "cors-like";
+}
+
 log.info("server", "Node Env: " + process.env.NODE_ENV);
 log.info("server", "Inside Container: " + (process.env.UPTIME_KUMA_IS_CONTAINER === "1"));
+log.info("server", "WebSocket Origin Check: " + process.env.UPTIME_KUMA_WS_ORIGIN_CHECK);
 
 log.info("server", "Importing Node libraries");
 const fs = require("fs");

programming_language: JavaScript
filename: uptime-kuma-server.js
change_type: ModificationType.MODIFY
diff: @@ -4,7 +4,7 @@ const fs = require("fs");
 const http = require("http");
 const { Server } = require("socket.io");
 const { R } = require("redbean-node");
-const { log } = require("../src/util");
+const { log, isDev } = require("../src/util");
 const Database = require("./database");
 const util = require("util");
 const { CacheableDnsHttpAgent } = require("./cacheable-dns-http-agent");
@@ -103,7 +103,41 @@ class UptimeKumaServer {
         UptimeKumaServer.monitorTypeList["real-browser"] = new RealBrowserMonitorType();
         UptimeKumaServer.monitorTypeList["tailscale-ping"] = new TailscalePing();
 
-        this.io = new Server(this.httpServer);
+        this.io = new Server(this.httpServer, {
+            allowRequest: (req, callback) => {
+                let isOriginValid = true;
+                const bypass = isDev || process.env.UPTIME_KUMA_WS_ORIGIN_CHECK === "bypass";
+
+                if (!bypass) {
+                    let host = req.headers.host;
+
+                    // If this is set, it means the request is from the browser
+                    let origin = req.headers.origin;
+
+                    // If this is from the browser, check if the origin is allowed
+                    if (origin) {
+                        try {
+                            let originURL = new URL(origin);
+
+                            if (host !== originURL.host) {
+                                isOriginValid = false;
+                                log.error("auth", `Origin (${origin}) does not match host (${host}), IP: ${req.socket.remoteAddress}`);
+                            }
+                        } catch (e) {
+                            // Invalid origin url, probably not from browser
+                            isOriginValid = false;
+                            log.error("auth", `Invalid origin url (${origin}), IP: ${req.socket.remoteAddress}`);
+                        }
+                    } else {
+                        log.info("auth", `Origin is not set, IP: ${req.socket.remoteAddress}`);
+                    }
+                } else {
+                    log.debug("auth", "Origin check is bypassed");
+                }
+
+                callback(null, isOriginValid);
+            }
+        });
     }
 
     /** Initialise app after the database has been set up */

programming_language: TypeScript