CVE-2023-30591: Improper Check for Unusual or Exceptional Conditions (CWE-754)
value: 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.
CWE 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.
CVSS v3 base score: 7.5
Published: 2023-09-29T06:15Z
Repository: https://github.com/NodeBB/NodeBB
Language: JavaScript
Files changed: src/socket.io/index.js
Commit date: 2023-03-28 08:08:59 -0400
Commit message: fix: don't crash on objects with toString property

Diff: diff --git a/src/socket.io/index.js b/src/socket.io/index.js
index 30e4e23581f..963267ed9a8 100644
--- a/src/socket.io/index.js
+++ b/src/socket.io/index.js
@@ -110,52 +110,53 @@ async function onConnect(socket) {
 async function onMessage(socket, payload) {
 	if (!payload.data.length) {
 		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]]');
 
 		if (Namespaces[namespace].before) {
 			await Namespaces[namespace].before(socket, eventName, params);

Vulnerable code: 	const eventName = payload.data[0];
	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}]]` });
	}
	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}]]` });
	}
	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();
	}
	try {
Fixed code: 	let eventName = payload.data[0];
	try {
		if (!eventName) {
			return winston.warn('[socket.io] Empty method name');
		}
		if (typeof eventName !== 'string') {
			eventName = typeof eventName;
			const escapedName = validator.escape(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))) {
				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}]]` });
		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();
		}