VYPR
Moderate severityNVD Advisory· Published Jul 25, 2023· Updated Oct 15, 2024

CVE-2023-2850

CVE-2023-2850

Description

NodeBB is affected by a Cross-Site WebSocket Hijacking vulnerability due to missing validation of the request origin. Exploitation of this vulnerability allows certain user information to be extracted by attacker.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

NodeBB lacks WebSocket origin validation, allowing attackers to hijack connections and extract user data via CSRF-like attack.

Vulnerability

Description CVE-2023-2850 is a Cross-Site WebSocket Hijacking vulnerability in NodeBB, a Node.js forum platform. The vulnerability arises because the application does not validate the Origin header during WebSocket handshakes, allowing an attacker to initiate a WebSocket connection from an untrusted site [1]. This is a classic CSRF-like attack vector targeting the WebSocket protocol.

Attack

Vector To exploit this vulnerability, an attacker must trick a logged-in NodeBB user into visiting a malicious webpage while the user's browser has a valid session cookie for the target NodeBB instance. The malicious page can then open a WebSocket connection to the vulnerable server, and because the server does not check origin, the connection is accepted [1]. The attacker can then send and receive messages over this WebSocket, impersonating the victim user.

Impact

Successful exploitation allows the attacker to extract certain user information, such as profile data or private messages, by leveraging the hijacked WebSocket session [1]. The attacker gains the same privileges as the victim user on the forum, potentially leading to further compromise.

Mitigation

The NodeBB development team has addressed this issue by backporting a fix that incorporates a CSRF token in the WebSocket connection setup [2]. The fix replaces the deprecated csurf library with csrf-sync and validates the token during the allowRequest callback of Socket.IO [2][4]. Users are advised to update to the latest patched version. No workaround is available for unpatched instances.

AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
nodebbnpm
>= 3.0.0, < 3.1.33.1.3
nodebbnpm
< 2.8.132.8.13

Affected products

2
  • ghsa-coords
    Range: >= 3.0.0, < 3.1.3
  • NodeBB/NodeBBv5
    Range: < 2.8.13

Patches

3
62e162cf1e73

fix: backport ws token fix

https://github.com/NodeBB/NodeBBBarış Soner UşaklıMay 15, 2023via ghsa
6 files changed · +47 18
  • public/src/sockets.js+3 0 modified
    @@ -15,6 +15,9 @@ app = window.app || {};
     		reconnectionDelay: config.reconnectionDelay,
     		transports: config.socketioTransports,
     		path: config.relative_path + '/socket.io',
    +		query: {
    +			_csrf: config.csrf_token,
    +		},
     	};
     
     	window.socket = io(config.websocketAddress, ioParams);
    
  • src/middleware/csrf.js+13 2 modified
    @@ -5,11 +5,22 @@ const { csrfSync } = require('csrf-sync');
     const {
     	generateToken,
     	csrfSynchronisedProtection,
    +	isRequestValid,
     } = csrfSync({
    -	size: 64
    +	getTokenFromRequest: (req) => {
    +		if (req.headers['x-csrf-token']) {
    +			return req.headers['x-csrf-token'];
    +		} else if (req.body && req.body.csrf_token) {
    +			return req.body.csrf_token;
    +		} else if (req.query) {
    +			return req.query._csrf;
    +		}
    +	},
    +	size: 64,
     });
     
     module.exports = {
     	generateToken,
     	csrfSynchronisedProtection,
    -};
    +	isRequestValid,
    +};
    \ No newline at end of file
    
  • src/middleware/index.js+1 1 modified
    @@ -2,11 +2,11 @@
     
     const async = require('async');
     const path = require('path');
    -const { csrfSynchronisedProtection } = require('./csrf');
     const validator = require('validator');
     const nconf = require('nconf');
     const toobusy = require('toobusy-js');
     const util = require('util');
    +const { csrfSynchronisedProtection } = require('./csrf');
     
     const plugins = require('../plugins');
     const meta = require('../meta');
    
  • src/socket.io/index.js+25 13 modified
    @@ -34,13 +34,25 @@ Sockets.init = async function (server) {
     		}
     	}
     
    -	io.use(authorize);
    -
     	io.on('connection', onConnection);
     
     	const opts = {
     		transports: nconf.get('socket.io:transports') || ['polling', 'websocket'],
     		cookie: false,
    +		allowRequest: (req, callback) => {
    +			authorize(req, (err) => {
    +				if (err) {
    +					return callback(err);
    +				}
    +				const csrf = require('../middleware/csrf');
    +				const isValid = csrf.isRequestValid({
    +					session: req.session || {},
    +					query: req._query,
    +					headers: req.headers,
    +				});
    +				callback(null, isValid);
    +			});
    +		},
     	};
     	/*
     	 * Restrict socket.io listener to cookie domain. If none is set, infer based on url.
    @@ -62,7 +74,11 @@ Sockets.init = async function (server) {
     };
     
     function onConnection(socket) {
    -	socket.ip = (socket.request.headers['x-forwarded-for'] || socket.request.connection.remoteAddress || '').split(',')[0];
    +	socket.uid = socket.request.uid;
    +	socket.ip = (
    +		socket.request.headers['x-forwarded-for'] ||
    +		socket.request.connection.remoteAddress || ''
    +	).split(',')[0];
     	socket.request.ip = socket.ip;
     	logger.io_one(socket, socket.uid);
     
    @@ -231,9 +247,7 @@ async function validateSession(socket, errorMsg) {
     
     const cookieParserAsync = util.promisify((req, callback) => cookieParser(req, {}, err => callback(err)));
     
    -async function authorize(socket, callback) {
    -	const { request } = socket;
    -
    +async function authorize(request, callback) {
     	if (!request) {
     		return callback(new Error('[[error:not-authorized]]'));
     	}
    @@ -246,15 +260,13 @@ async function authorize(socket, callback) {
     	});
     
     	const sessionData = await getSessionAsync(sessionId);
    -
    +	request.session = sessionData;
    +	let uid = 0;
     	if (sessionData && sessionData.passport && sessionData.passport.user) {
    -		request.session = sessionData;
    -		socket.uid = parseInt(sessionData.passport.user, 10);
    -	} else {
    -		socket.uid = 0;
    +		uid = parseInt(sessionData.passport.user, 10);
     	}
    -	request.uid = socket.uid;
    -	callback();
    +	request.uid = uid;
    +	callback(null, uid);
     }
     
     Sockets.in = function (room) {
    
  • test/helpers/index.js+4 1 modified
    @@ -95,7 +95,7 @@ helpers.logoutUser = function (jar, callback) {
     	});
     };
     
    -helpers.connectSocketIO = function (res, callback) {
    +helpers.connectSocketIO = function (res, csrf_token, callback) {
     	const io = require('socket.io-client');
     	let cookies = res.headers['set-cookie'];
     	cookies = cookies.filter(c => /express.sid=[^;]+;/.test(c));
    @@ -106,6 +106,9 @@ helpers.connectSocketIO = function (res, callback) {
     			Origin: nconf.get('url'),
     			Cookie: cookie,
     		},
    +		query: {
    +			_csrf: csrf_token,
    +		},
     	});
     
     	socket.on('connect', () => {
    
  • test/socket.io.js+1 1 modified
    @@ -73,7 +73,7 @@ describe('socket.io', () => {
     			}, (err, res) => {
     				assert.ifError(err);
     
    -				helpers.connectSocketIO(res, (err, _io) => {
    +				helpers.connectSocketIO(res, body.csrf_token, (err, _io) => {
     					io = _io;
     					assert.ifError(err);
     
    
51096ad2345f

poc: use csrf_token in ws handshake (#11573)

https://github.com/NodeBB/NodeBBBarış Soner UşaklıMay 15, 2023via ghsa
5 files changed · +38 16
  • public/src/sockets.js+3 0 modified
    @@ -15,6 +15,9 @@ app = window.app || {};
     		reconnectionDelay: config.reconnectionDelay,
     		transports: config.socketioTransports,
     		path: config.relative_path + '/socket.io',
    +		query: {
    +			_csrf: config.csrf_token,
    +		},
     	};
     
     	window.socket = io(config.websocketAddress, ioParams);
    
  • src/middleware/csrf.js+5 1 modified
    @@ -5,12 +5,15 @@ const { csrfSync } = require('csrf-sync');
     const {
     	generateToken,
     	csrfSynchronisedProtection,
    +	isRequestValid,
     } = csrfSync({
     	getTokenFromRequest: (req) => {
     		if (req.headers['x-csrf-token']) {
     			return req.headers['x-csrf-token'];
    -		} else if (req.body.csrf_token) {
    +		} else if (req.body && req.body.csrf_token) {
     			return req.body.csrf_token;
    +		} else if (req.query) {
    +			return req.query._csrf;
     		}
     	},
     	size: 64,
    @@ -19,4 +22,5 @@ const {
     module.exports = {
     	generateToken,
     	csrfSynchronisedProtection,
    +	isRequestValid,
     };
    
  • src/socket.io/index.js+25 13 modified
    @@ -34,13 +34,25 @@ Sockets.init = async function (server) {
     		}
     	}
     
    -	io.use(authorize);
    -
     	io.on('connection', onConnection);
     
     	const opts = {
     		transports: nconf.get('socket.io:transports') || ['polling', 'websocket'],
     		cookie: false,
    +		allowRequest: (req, callback) => {
    +			authorize(req, (err) => {
    +				if (err) {
    +					return callback(err);
    +				}
    +				const csrf = require('../middleware/csrf');
    +				const isValid = csrf.isRequestValid({
    +					session: req.session || {},
    +					query: req._query,
    +					headers: req.headers,
    +				});
    +				callback(null, isValid);
    +			});
    +		},
     	};
     	/*
     	 * Restrict socket.io listener to cookie domain. If none is set, infer based on url.
    @@ -62,7 +74,11 @@ Sockets.init = async function (server) {
     };
     
     function onConnection(socket) {
    -	socket.ip = (socket.request.headers['x-forwarded-for'] || socket.request.connection.remoteAddress || '').split(',')[0];
    +	socket.uid = socket.request.uid;
    +	socket.ip = (
    +		socket.request.headers['x-forwarded-for'] ||
    +		socket.request.connection.remoteAddress || ''
    +	).split(',')[0];
     	socket.request.ip = socket.ip;
     	logger.io_one(socket, socket.uid);
     
    @@ -231,9 +247,7 @@ async function validateSession(socket, errorMsg) {
     
     const cookieParserAsync = util.promisify((req, callback) => cookieParser(req, {}, err => callback(err)));
     
    -async function authorize(socket, callback) {
    -	const { request } = socket;
    -
    +async function authorize(request, callback) {
     	if (!request) {
     		return callback(new Error('[[error:not-authorized]]'));
     	}
    @@ -246,15 +260,13 @@ async function authorize(socket, callback) {
     	});
     
     	const sessionData = await getSessionAsync(sessionId);
    -
    +	request.session = sessionData;
    +	let uid = 0;
     	if (sessionData && sessionData.passport && sessionData.passport.user) {
    -		request.session = sessionData;
    -		socket.uid = parseInt(sessionData.passport.user, 10);
    -	} else {
    -		socket.uid = 0;
    +		uid = parseInt(sessionData.passport.user, 10);
     	}
    -	request.uid = socket.uid;
    -	callback();
    +	request.uid = uid;
    +	callback(null, uid);
     }
     
     Sockets.in = function (room) {
    
  • test/helpers/index.js+4 1 modified
    @@ -96,7 +96,7 @@ helpers.logoutUser = function (jar, callback) {
     	});
     };
     
    -helpers.connectSocketIO = function (res, callback) {
    +helpers.connectSocketIO = function (res, csrf_token, callback) {
     	const io = require('socket.io-client');
     	let cookies = res.headers['set-cookie'];
     	cookies = cookies.filter(c => /express.sid=[^;]+;/.test(c));
    @@ -107,6 +107,9 @@ helpers.connectSocketIO = function (res, callback) {
     			Origin: nconf.get('url'),
     			Cookie: cookie,
     		},
    +		query: {
    +			_csrf: csrf_token,
    +		},
     	});
     
     	socket.on('connect', () => {
    
  • test/socket.io.js+1 1 modified
    @@ -73,7 +73,7 @@ describe('socket.io', () => {
     			}, (err, res) => {
     				assert.ifError(err);
     
    -				helpers.connectSocketIO(res, (err, _io) => {
    +				helpers.connectSocketIO(res, body.csrf_token, (err, _io) => {
     					io = _io;
     					assert.ifError(err);
     
    
a5d92da9ddac

Replace csurf with csrf-sync

https://github.com/NodeBB/NodeBBpsibeanJan 29, 2023via ghsa
5 files changed · +22 5
  • install/package.json+1 1 modified
    @@ -55,7 +55,7 @@
             "cookie-parser": "1.4.6",
             "cron": "2.3.0",
             "cropperjs": "1.5.13",
    -        "csurf": "1.11.0",
    +        "csrf-sync": "4.0.0",
             "daemon": "1.1.0",
             "diff": "5.1.0",
             "esbuild": "0.16.10",
    
  • src/controllers/api.js+2 1 modified
    @@ -9,6 +9,7 @@ const categories = require('../categories');
     const plugins = require('../plugins');
     const translator = require('../translator');
     const languages = require('../languages');
    +const { generateToken } = require('../middleware/csrf');
     
     const apiController = module.exports;
     
    @@ -64,7 +65,7 @@ apiController.loadConfig = async function (req) {
     		'cache-buster': meta.config['cache-buster'] || '',
     		topicPostSort: meta.config.topicPostSort || 'oldest_to_newest',
     		categoryTopicSort: meta.config.categoryTopicSort || 'newest_to_oldest',
    -		csrf_token: req.uid >= 0 && req.csrfToken && req.csrfToken(),
    +		csrf_token: req.uid >= 0 ? generateToken(req) : undefined,
     		searchEnabled: plugins.hooks.hasListeners('filter:search.query'),
     		searchDefaultInQuick: meta.config.searchDefaultInQuick || 'titles',
     		bootswatchSkin: meta.config.bootswatchSkin || '',
    
  • src/middleware/csrf.js+15 0 added
    @@ -0,0 +1,15 @@
    +'use strict';
    +
    +const { csrfSync } = require('csrf-sync');
    +
    +const {
    +	generateToken,
    +	csrfSynchronisedProtection,
    +} = csrfSync({
    +	size: 64
    +});
    +
    +module.exports = {
    +	generateToken,
    +	csrfSynchronisedProtection,
    +};
    
  • src/middleware/index.js+2 2 modified
    @@ -2,7 +2,7 @@
     
     const async = require('async');
     const path = require('path');
    -const csrf = require('csurf');
    +const { csrfSynchronisedProtection } = require('./csrf');
     const validator = require('validator');
     const nconf = require('nconf');
     const toobusy = require('toobusy-js');
    @@ -34,7 +34,7 @@ middleware.regexes = {
     	timestampedUpload: /^\d+-.+$/,
     };
     
    -const csrfMiddleware = csrf();
    +const csrfMiddleware = csrfSynchronisedProtection;
     
     middleware.applyCSRF = function (req, res, next) {
     	if (req.uid >= 0) {
    
  • src/routes/authentication.js+2 1 modified
    @@ -10,6 +10,7 @@ const meta = require('../meta');
     const controllers = require('../controllers');
     const helpers = require('../controllers/helpers');
     const plugins = require('../plugins');
    +const { generateToken } = require('../middleware/csrf');
     
     let loginStrategies = [];
     
    @@ -108,7 +109,7 @@ Auth.reloadRoutes = async function (params) {
     				};
     
     				if (strategy.checkState !== false) {
    -					req.session.ssoState = req.csrfToken && req.csrfToken();
    +					req.session.ssoState = generateToken(req, true);
     					opts.state = req.session.ssoState;
     				}
     
    

Vulnerability mechanics

Generated 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.