CVE-2023-30591 (published 2023-09-29T06:15Z)

Denial-of-service in NodeBB <= v2.8.10 allows unauthenticated attackers to trigger a crash, when invoking `eventName.startsWith()` or `eventName.toString()`, while processing Socket.IO messages via crafted Socket.IO messages containing array or object type for the event name respectively.

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

Weakness information:
cwe_id: CWE-754
cwe_name: Improper Check for Unusual or Exceptional Conditions
description: The product does not check or incorrectly checks for unusual or exceptional conditions that are not expected to occur frequently during day to day operation of the product.
extended_description: 
            The programmer may assume that certain events or conditions will never occur or do not need to be worried about, such as low memory conditions, lack of access to resources due to restrictive permissions, or misbehaving clients or components. However, attackers may intentionally trigger these unusual conditions, thus violating the programmer's assumptions, possibly introducing instability, incorrect behavior, or a vulnerability.
            Note that this entry is not exclusively about the use of exceptions and exception handling, which are mechanisms for both checking and handling unusual or unexpected conditions.
         
         

Fix information:
repo_url: https://github.com/NodeBB/NodeBB
commit_details: repo_url: https://github.com/NodeBB/NodeBB
author: Barış Soner Uşaklı
author_date: 2023-03-28 08:08:59-04:00
msg: fix: don't crash on objects with toString property
repository: repo_url: https://github.com/NodeBB/NodeBB
repo_name: NodeBB/NodeBB
description: Node.js based forum software built for the modern web
repo_language: JavaScript
owner: NodeBB
file_changes: filename: index.js
change_type: ModificationType.MODIFY
diff: @@ -112,48 +112,49 @@ async function onMessage(socket, payload) {
 		return winston.warn('[socket.io] Empty payload');
 	}
 
-	const eventName = payload.data[0];
+	let eventName = payload.data[0];
 	const params = typeof payload.data[1] === 'function' ? {} : payload.data[1];
 	const callback = typeof payload.data[payload.data.length - 1] === 'function' ? payload.data[payload.data.length - 1] : function () {};
 
-	if (!eventName) {
-		return winston.warn('[socket.io] Empty method name');
-	}
-
-	if (typeof eventName !== 'string') {
-		const escapedName = validator.escape(String(eventName));
-		return callback({ message: `[[error:invalid-event, ${escapedName}]]` });
-	}
+	try {
+		if (!eventName) {
+			return winston.warn('[socket.io] Empty method name');
+		}
 
-	const parts = eventName.split('.');
-	const namespace = parts[0];
-	const methodToCall = parts.reduce((prev, cur) => {
-		if (prev !== null && prev[cur] && (!prev.hasOwnProperty || prev.hasOwnProperty(cur))) {
-			return prev[cur];
+		if (typeof eventName !== 'string') {
+			eventName = typeof eventName;
+			const escapedName = validator.escape(eventName);
+			return callback({ message: `[[error:invalid-event, ${escapedName}]]` });
 		}
-		return null;
-	}, Namespaces);
 
-	if (!methodToCall || typeof methodToCall !== 'function') {
-		if (process.env.NODE_ENV === 'development') {
-			winston.warn(`[socket.io] Unrecognized message: ${eventName}`);
+		const parts = eventName.split('.');
+		const namespace = parts[0];
+		const methodToCall = parts.reduce((prev, cur) => {
+			if (prev !== null && prev[cur] && (!prev.hasOwnProperty || prev.hasOwnProperty(cur))) {
+				return prev[cur];
+			}
+			return null;
+		}, Namespaces);
+
+		if (!methodToCall || typeof methodToCall !== 'function') {
+			if (process.env.NODE_ENV === 'development') {
+				winston.warn(`[socket.io] Unrecognized message: ${eventName}`);
+			}
+			const escapedName = validator.escape(String(eventName));
+			return callback({ message: `[[error:invalid-event, ${escapedName}]]` });
 		}
-		const escapedName = validator.escape(String(eventName));
-		return callback({ message: `[[error:invalid-event, ${escapedName}]]` });
-	}
 
-	socket.previousEvents = socket.previousEvents || [];
-	socket.previousEvents.push(eventName);
-	if (socket.previousEvents.length > 20) {
-		socket.previousEvents.shift();
-	}
+		socket.previousEvents = socket.previousEvents || [];
+		socket.previousEvents.push(eventName);
+		if (socket.previousEvents.length > 20) {
+			socket.previousEvents.shift();
+		}
 
-	if (!eventName.startsWith('admin.') && ratelimit.isFlooding(socket)) {
-		winston.warn(`[socket.io] Too many emits! Disconnecting uid : ${socket.uid}. Events : ${socket.previousEvents}`);
-		return socket.disconnect();
-	}
+		if (!eventName.startsWith('admin.') && ratelimit.isFlooding(socket)) {
+			winston.warn(`[socket.io] Too many emits! Disconnecting uid : ${socket.uid}. Events : ${socket.previousEvents}`);
+			return socket.disconnect();
+		}
 
-	try {
 		await checkMaintenance(socket);
 		await validateSession(socket, '[[error:revalidate-failure]]');
 

programming_language: JavaScript
repo_url: https://github.com/NodeBB/NodeBB
commit_details: repo_url: https://github.com/NodeBB/NodeBB
author: Julian Lam
author_date: 2023-03-27 10:55:40-04:00
msg: fix: #11403, remove loader.js crash counter logic
repository: repo_url: https://github.com/NodeBB/NodeBB
repo_name: NodeBB/NodeBB
description: Node.js based forum software built for the modern web
repo_language: JavaScript
owner: NodeBB
file_changes: filename: loader.js
change_type: ModificationType.MODIFY
diff: @@ -30,9 +30,7 @@ const output = logrotate({ file: outputLogFilePath, size: '1m', keep: 3, compres
 const silent = nconf.get('silent') === 'false' ? false : nconf.get('silent') !== false;
 let numProcs;
 const workers = [];
-const Loader = {
-	timesStarted: 0,
-};
+const Loader = {};
 const appPath = path.join(__dirname, 'app.js');
 
 Loader.init = function () {
@@ -57,21 +55,6 @@ Loader.displayStartupMessages = function () {
 
 Loader.addWorkerEvents = function (worker) {
 	worker.on('exit', (code, signal) => {
-		if (code !== 0) {
-			if (Loader.timesStarted < numProcs * 3) {
-				Loader.timesStarted += 1;
-				if (Loader.crashTimer) {
-					clearTimeout(Loader.crashTimer);
-				}
-				Loader.crashTimer = setTimeout(() => {
-					Loader.timesStarted = 0;
-				}, 10000);
-			} else {
-				console.log(`${numProcs * 3} restarts in 10 seconds, most likely an error on startup. Halting.`);
-				process.exit();
-			}
-		}
-
 		console.log(`[cluster] Child Process (${worker.pid}) has exited (code: ${code}, signal: ${signal})`);
 		if (!(worker.suicide || code === 0)) {
 			console.log('[cluster] Spinning up another process...');

programming_language: JavaScript
repo_url: https://github.com/NodeBB/NodeBB
commit_details: repo_url: https://github.com/NodeBB/NodeBB
author: Barış Soner Uşaklı
author_date: 2023-03-27 10:38:53-04:00
msg: fix: don't crash if event name is not a string
repository: repo_url: https://github.com/NodeBB/NodeBB
repo_name: NodeBB/NodeBB
description: Node.js based forum software built for the modern web
repo_language: JavaScript
owner: NodeBB
file_changes: filename: index.js
change_type: ModificationType.MODIFY
diff: @@ -120,7 +120,12 @@ async function onMessage(socket, payload) {
 		return winston.warn('[socket.io] Empty method name');
 	}
 
-	const parts = eventName.toString().split('.');
+	if (typeof eventName !== 'string') {
+		const escapedName = validator.escape(String(eventName));
+		return callback({ message: `[[error:invalid-event, ${escapedName}]]` });
+	}
+
+	const parts = eventName.split('.');
 	const namespace = parts[0];
 	const methodToCall = parts.reduce((prev, cur) => {
 		if (prev !== null && prev[cur] && (!prev.hasOwnProperty || prev.hasOwnProperty(cur))) {

programming_language: JavaScript
filename: socket.io.js
change_type: ModificationType.MODIFY
diff: @@ -107,6 +107,15 @@ describe('socket.io', () => {
 		});
 	});
 
+	it('should return error for invalid eventName type', (done) => {
+		const eventName = ['topics.loadMoreTags'];
+		io.emit(eventName, function (err) {
+			const eventAsString = String(eventName);
+			assert.strictEqual(err.message, `[[error:invalid-event, ${eventAsString}]]`);
+			done();
+		});
+	});
+
 	it('should get installed themes', (done) => {
 		const themes = ['nodebb-theme-lavender', 'nodebb-theme-persona', 'nodebb-theme-vanilla'];
 		io.emit('admin.themes.getInstalled', (err, data) => {

programming_language: TypeScript