diff --git a/install/package.json b/install/package.json index 0848d671ddf0..fa14c5eb7a77 100644 --- a/install/package.json +++ b/install/package.json @@ -54,7 +54,7 @@ "cookie-parser": "1.4.6", "cron": "1.8.2", "cropperjs": "1.5.12", - "csurf": "1.11.0", + "csrf-sync": "4.0.1", "daemon": "1.1.0", "diff": "5.0.0", "express": "4.18.0", diff --git a/public/src/sockets.js b/public/src/sockets.js index 37698f1a4ef4..cf617c2dc367 100644 --- a/public/src/sockets.js +++ b/public/src/sockets.js @@ -12,6 +12,9 @@ socket = window.socket; reconnectionDelay: config.reconnectionDelay, transports: config.socketioTransports, path: config.relative_path + '/socket.io', + query: { + _csrf: config.csrf_token, + }, }; socket = io(config.websocketAddress, ioParams); diff --git a/src/controllers/api.js b/src/controllers/api.js index 7474f6e7a041..a24051f0905e 100644 --- a/src/controllers/api.js +++ b/src/controllers/api.js @@ -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) : false, searchEnabled: plugins.hooks.hasListeners('filter:search.query'), searchDefaultInQuick: meta.config.searchDefaultInQuick || 'titles', bootswatchSkin: meta.config.bootswatchSkin || '', diff --git a/src/middleware/csrf.js b/src/middleware/csrf.js new file mode 100644 index 000000000000..cf85e2a0b4ed --- /dev/null +++ b/src/middleware/csrf.js @@ -0,0 +1,28 @@ +'use strict'; + +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 && req.body.csrf_token) { + return req.body.csrf_token; + } else if (req.body && req.body._csrf) { + return req.body._csrf; + } else if (req.query && req.query._csrf) { + return req.query._csrf; + } + }, + size: 64, +}); + +module.exports = { + generateToken, + csrfSynchronisedProtection, + isRequestValid, +}; diff --git a/src/middleware/index.js b/src/middleware/index.js index a31cc4430d28..864153ad1705 100644 --- a/src/middleware/index.js +++ b/src/middleware/index.js @@ -2,13 +2,14 @@ const async = require('async'); const path = require('path'); -const csrf = require('csurf'); const validator = require('validator'); const nconf = require('nconf'); const toobusy = require('toobusy-js'); const LRU = require('lru-cache'); const util = require('util'); +const { csrfSynchronisedProtection } = require('./csrf'); + const plugins = require('../plugins'); const meta = require('../meta'); const user = require('../user'); @@ -34,7 +35,7 @@ middleware.regexes = { timestampedUpload: /^\d+-.+$/, }; -const csrfMiddleware = csrf(); +const csrfMiddleware = csrfSynchronisedProtection; middleware.applyCSRF = function (req, res, next) { if (req.uid >= 0) { diff --git a/src/routes/authentication.js b/src/routes/authentication.js index b2b4f0bf6d3b..f8092f28ab21 100644 --- a/src/routes/authentication.js +++ b/src/routes/authentication.js @@ -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 = []; @@ -94,7 +95,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; } diff --git a/src/socket.io/index.js b/src/socket.io/index.js index 60c7a8cd2779..a62d168f530a 100644 --- a/src/socket.io/index.js +++ b/src/socket.io/index.js @@ -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); @@ -225,9 +241,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]]')); } @@ -240,15 +254,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) { diff --git a/test/helpers/index.js b/test/helpers/index.js index e9df664ee363..add46742d245 100644 --- a/test/helpers/index.js +++ b/test/helpers/index.js @@ -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', () => { diff --git a/test/socket.io.js b/test/socket.io.js index e38777c1b33e..9de7cec08486 100644 --- a/test/socket.io.js +++ b/test/socket.io.js @@ -74,7 +74,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);