diff --git a/package.json b/package.json
index fdb2acb63..cc6e7ed75 100644
--- a/package.json
+++ b/package.json
@@ -39,6 +39,8 @@
"connect-mongo": "4.6.0",
"cookie": "0.4.2",
"cookie-parser": "1.4.6",
+ "cookie-signature": "1.0.6",
+ "csrf": "3.1.0",
"csv": "6.0.5",
"debug": "4.3.3",
"dive": "0.5.0",
@@ -50,6 +52,7 @@
"fs-extra": "10.0.0",
"handlebars": "4.7.7",
"html-to-text": "8.1.0",
+ "http-errors": "~1.7.3",
"imap": "0.8.19",
"immutable": "4.0.0",
"imports-loader": "3.1.1",
diff --git a/src/client/containers/Settings/Server/index.jsx b/src/client/containers/Settings/Server/index.jsx
index 7f75c98eb..69b3e0590 100644
--- a/src/client/containers/Settings/Server/index.jsx
+++ b/src/client/containers/Settings/Server/index.jsx
@@ -59,11 +59,20 @@ class ServerSettingsController extends React.Component {
restartServer () {
this.setState({ restarting: true })
+ const token = document.querySelector('meta[name="csrf-token"]').getAttribute('content')
axios
- .get('/api/v1/admin/restart')
+ .post(
+ '/api/v1/admin/restart',
+ {},
+ {
+ headers: {
+ 'CSRF-TOKEN': token
+ }
+ }
+ )
.catch(error => {
helpers.hideLoader()
- Log.error(error.response)
+ Log.error(error.responseText)
Log.error('Unable to restart server. Server must run under PM2 and Account must have admin rights.')
helpers.UI.showSnackbar('Unable to restart server. Are you an Administrator?', true)
})
diff --git a/src/controllers/api/v1/routes.js b/src/controllers/api/v1/routes.js
index 0cb0267aa..5499c6360 100644
--- a/src/controllers/api/v1/routes.js
+++ b/src/controllers/api/v1/routes.js
@@ -136,8 +136,8 @@ module.exports = function (middleware, router, controllers) {
router.delete('/api/v1/users/:username', apiv1, canUser('accounts:delete'), apiCtrl.users.deleteUser)
router.post('/api/v1/users/:id/generateapikey', apiv1, apiCtrl.users.generateApiKey)
router.post('/api/v1/users/:id/removeapikey', apiv1, apiCtrl.users.removeApiKey)
- router.post('/api/v1/users/:id/generatel2auth', apiv1, apiCtrl.users.generateL2Auth)
- router.post('/api/v1/users/:id/removel2auth', apiv1, apiCtrl.users.removeL2Auth)
+ router.post('/api/v1/users/:id/generatel2auth', apiv1, middleware.csrfCheck, apiCtrl.users.generateL2Auth)
+ router.post('/api/v1/users/:id/removel2auth', apiv1, middleware.csrfCheck, apiCtrl.users.removeL2Auth)
// Messages
router.get('/api/v1/messages', apiv1, apiCtrl.messages.get)
diff --git a/src/dependencies/csrf-td/index.js b/src/dependencies/csrf-td/index.js
new file mode 100644
index 000000000..35990c8a4
--- /dev/null
+++ b/src/dependencies/csrf-td/index.js
@@ -0,0 +1,314 @@
+/*!
+ * csurf
+ * Copyright(c) 2011 Sencha Inc.
+ * Copyright(c) 2014 Jonathan Ong
+ * Copyright(c) 2014-2016 Douglas Christopher Wilson
+ * MIT Licensed
+ *
+ * Modified for Trudesk by Chris Brame (2022)
+ */
+
+'use strict'
+
+/**
+ * Module dependencies.
+ * @private
+ */
+
+const Cookie = require('cookie')
+const createError = require('http-errors')
+const sign = require('cookie-signature').sign
+const Tokens = require('csrf')
+
+/**
+ * Module exports.
+ * @public
+ */
+
+const csurf = {}
+
+/**
+ * CSRF protection middleware.
+ *
+ * This middleware adds a `req.csrfToken()` function to make a token
+ * which should be added to requests which mutate
+ * state, within a hidden form field, query-string etc. This
+ * token is validated against the visitor's session.
+ *
+ * @param {Object} options
+ * @return {Function} middleware
+ * @public
+ */
+
+let cookie, sessionKey, value, tokens, ignoreMethods, opts
+
+csurf.init = function (options) {
+ opts = options || {}
+
+ // get cookie options
+ cookie = getCookieOptions(opts.cookie)
+
+ // get session options
+ sessionKey = opts.sessionKey || 'session'
+
+ // get value getter
+ value = opts.value || defaultValue
+
+ // token repo
+ tokens = new Tokens(opts)
+
+ // ignored methods
+ ignoreMethods = opts.ignoreMethods === undefined ? ['GET', 'HEAD', 'OPTIONS'] : opts.ignoreMethods
+
+ if (!Array.isArray(ignoreMethods)) {
+ throw new TypeError('option ignoreMethods must be an array')
+ }
+}
+
+function generateSecret (req, res) {
+ // get the secret from the request
+ let secret = getSecret(req, sessionKey, cookie)
+
+ // generate & set secret
+ if (!secret) {
+ secret = tokens.secretSync()
+ setSecret(req, res, sessionKey, secret, cookie)
+ }
+
+ return secret
+}
+
+csurf.generateToken = function (req, res, next) {
+ // validate the configuration against request
+ if (!verifyConfiguration(req, sessionKey, cookie)) {
+ throw new Error('misconfigured csrf')
+ }
+
+ // eslint-disable-next-line prefer-const
+ let token
+ let secret = generateSecret(req, res)
+
+ let sec = !cookie ? getSecret(req, sessionKey, cookie) : secret
+
+ // use cached token if secret has not changed
+ if (token && sec === secret) {
+ return token
+ }
+
+ // generate & set new secret
+ if (sec === undefined) {
+ sec = tokens.secretSync()
+ setSecret(req, res, sessionKey, sec, cookie)
+ }
+
+ // update changed secret
+ secret = sec
+
+ // create new token
+ token = tokens.create(secret)
+ req.csrfToken = token
+
+ next()
+}
+
+csurf.middleware = function csrf (req, res, next) {
+ // generate lookup
+ const ignoreMethod = getIgnoredMethods(ignoreMethods)
+
+ // validate the configuration against request
+ if (!verifyConfiguration(req, sessionKey, cookie)) {
+ return next(new Error('misconfigured csrf'))
+ }
+
+ const secret = generateSecret(req, res)
+
+ // verify the incoming token
+ if (!ignoreMethod[req.method] && !tokens.verify(secret, value(req))) {
+ return next(
+ createError(403, 'invalid csrf token', {
+ code: 'EBADCSRFTOKEN'
+ })
+ )
+ }
+
+ next()
+}
+
+/**
+ * Default value function, checking the `req.body`
+ * and `req.query` for the CSRF token.
+ *
+ * @param {IncomingMessage} req
+ * @return {String}
+ * @api private
+ */
+
+function defaultValue (req) {
+ return (
+ (req.body && req.body._csrf) ||
+ (req.query && req.query._csrf) ||
+ req.headers['csrf-token'] ||
+ req.headers['xsrf-token'] ||
+ req.headers['x-csrf-token'] ||
+ req.headers['x-xsrf-token']
+ )
+}
+
+/**
+ * Get options for cookie.
+ *
+ * @param {boolean|object} [options]
+ * @returns {object}
+ * @api private
+ */
+
+function getCookieOptions (options) {
+ if (options !== true && typeof options !== 'object') {
+ return undefined
+ }
+
+ const opts = Object.create(null)
+
+ // defaults
+ opts.key = '_csrf'
+ opts.path = '/'
+
+ if (options && typeof options === 'object') {
+ for (const prop in options) {
+ const val = options[prop]
+
+ if (val !== undefined) {
+ opts[prop] = val
+ }
+ }
+ }
+
+ return opts
+}
+
+/**
+ * Get a lookup of ignored methods.
+ *
+ * @param {array} methods
+ * @returns {object}
+ * @api private
+ */
+
+function getIgnoredMethods (methods) {
+ const obj = Object.create(null)
+
+ for (let i = 0; i < methods.length; i++) {
+ const method = methods[i].toUpperCase()
+ obj[method] = true
+ }
+
+ return obj
+}
+
+/**
+ * Get the token secret from the request.
+ *
+ * @param {IncomingMessage} req
+ * @param {String} sessionKey
+ * @param {Object} [cookie]
+ * @api private
+ */
+
+function getSecret (req, sessionKey, cookie) {
+ // get the bag & key
+ const bag = getSecretBag(req, sessionKey, cookie)
+ const key = cookie ? cookie.key : 'csrfSecret'
+
+ if (!bag) {
+ throw new Error('misconfigured csrf')
+ }
+
+ // return secret from bag
+ return bag[key]
+}
+
+/**
+ * Get the token secret bag from the request.
+ *
+ * @param {IncomingMessage} req
+ * @param {String} sessionKey
+ * @param {Object} [cookie]
+ * @api private
+ */
+
+function getSecretBag (req, sessionKey, cookie) {
+ if (cookie) {
+ // get secret from cookie
+ const cookieKey = cookie.signed ? 'signedCookies' : 'cookies'
+
+ return req[cookieKey]
+ } else {
+ // get secret from session
+ return req[sessionKey]
+ }
+}
+
+/**
+ * Set a cookie on the HTTP response.
+ *
+ * @param {OutgoingMessage} res
+ * @param {string} name
+ * @param {string} val
+ * @param {Object} [options]
+ * @api private
+ */
+
+function setCookie (res, name, val, options) {
+ const data = Cookie.serialize(name, val, options)
+
+ const prev = res.getHeader('set-cookie') || []
+ const header = Array.isArray(prev) ? prev.concat(data) : [prev, data]
+
+ res.setHeader('set-cookie', header)
+}
+
+/**
+ * Set the token secret on the request.
+ *
+ * @param {IncomingMessage} req
+ * @param {OutgoingMessage} res
+ * @param {string} sessionKey
+ * @param {string} val
+ * @param {Object} [cookie]
+ * @api private
+ */
+
+function setSecret (req, res, sessionKey, val, cookie) {
+ if (cookie) {
+ // set secret on cookie
+ let value = val
+
+ if (cookie.signed) {
+ value = 's:' + sign(val, req.secret)
+ }
+
+ setCookie(res, cookie.key, value, cookie)
+ } else {
+ // set secret on session
+ req[sessionKey].csrfSecret = val
+ }
+}
+
+/**
+ * Verify the configuration against the request.
+ * @private
+ */
+
+function verifyConfiguration (req, sessionKey, cookie) {
+ if (!getSecretBag(req, sessionKey, cookie)) {
+ return false
+ }
+
+ if (cookie && cookie.signed && !req.secret) {
+ return false
+ }
+
+ return true
+}
+
+module.exports = csurf
diff --git a/src/middleware/index.js b/src/middleware/index.js
index 17f9744af..1cb07dff3 100644
--- a/src/middleware/index.js
+++ b/src/middleware/index.js
@@ -99,6 +99,9 @@ module.exports = function (app, db, callback) {
// CORS
app.use(allowCrossDomain)
+ const csrf = require('../dependencies/csrf-td')
+ csrf.init()
+ app.use(csrf.generateToken)
// Maintenance Mode
app.use(function (req, res, next) {
diff --git a/src/middleware/middleware.js b/src/middleware/middleware.js
index 9fd5273b6..3d966f459 100644
--- a/src/middleware/middleware.js
+++ b/src/middleware/middleware.js
@@ -17,7 +17,9 @@
var _ = require('lodash')
var db = require('../database')
var mongoose = require('mongoose')
-var winston = require('winston')
+var winston = require('../logger')
+const csrf = require('../dependencies/csrf-td')
+const viewdata = require('../helpers/viewdata')
var middleware = {}
@@ -111,8 +113,8 @@ middleware.ensurel2Auth = function (req, res, next) {
// Common
middleware.loadCommonData = function (req, res, next) {
- var viewdata = require('../helpers/viewdata')
viewdata.getData(req, function (data) {
+ data.csrfToken = req.csrfToken
req.viewdata = data
return next()
@@ -250,6 +252,11 @@ middleware.isAgent = function (req, res, next) {
middleware.isSupport = middleware.isAgent
+middleware.csrfCheck = function (req, res, next) {
+ csrf.init()
+ return csrf.middleware(req, res, next)
+}
+
module.exports = function () {
return middleware
}
diff --git a/src/public/js/angularjs/controllers/profile.js b/src/public/js/angularjs/controllers/profile.js
index 65eb52db2..2396cd364 100644
--- a/src/public/js/angularjs/controllers/profile.js
+++ b/src/public/js/angularjs/controllers/profile.js
@@ -24,10 +24,12 @@ define([
], function (angular, _, $, helpers, UIKit) {
return angular
.module('trudesk.controllers.profile', ['trudesk.services.session'])
- .controller('profileCtrl', function (SessionService, $scope, $window, $http, $log, $timeout) {
+ .controller('profileCtrl', function (SessionService, $scope, $window, $document, $http, $log, $timeout) {
+ var otpEnabled = false
$scope.init = function () {
// Fix Inputs if input is preloaded with a value
fixInputLabels()
+ otpEnabled = $scope.otpEnabled
}
function fixInputLabels () {
@@ -164,7 +166,7 @@ define([
var $qrCode = $totpSettings.find('#totp-qrcode')
event.preventDefault()
- if ($scope.otpEnabled) {
+ if (otpEnabled) {
UIKit.modal.confirm(
'WARNING: Disabling Two Factor Authentication will remove your shared secret. A new key will generate when re-enabled.
' +
'Are you sure you want to disable two factor authentication?',
@@ -180,7 +182,7 @@ define([
$qrCode.find('canvas').remove()
$tOTPKey.val()
$timeout(function () {
- $scope.otpEnabled = false
+ otpEnabled = false
}, 0)
})
})
@@ -198,7 +200,7 @@ define([
}
$timeout(function () {
- $scope.otpEnabled = true
+ otpEnabled = true
angular.element(event.target).prop('checked', true)
}, 0)
@@ -234,29 +236,39 @@ define([
return helpers.UI.showSnackbar('Unable to get user ID.', true)
}
- $http.post('/api/v1/users/' + id + '/generatel2auth').then(
- function success (response) {
- if (!response.data.success) {
- helpers.UI.showSnackbar('Error: Unknown error has occurred.', true)
- if (_.isFunction(completed)) {
- return completed('Error: Unknown error has occurred.')
+ $http
+ .post(
+ '/api/v1/users/' + id + '/generatel2auth',
+ {},
+ {
+ headers: {
+ 'CSRF-TOKEN': $document[0].querySelector('meta[name="csrf-token"]').getAttribute('content')
}
- } else {
- // Success
+ }
+ )
+ .then(
+ function success (response) {
+ if (!response.data.success) {
+ helpers.UI.showSnackbar('Error: Unknown error has occurred.', true)
+ if (_.isFunction(completed)) {
+ return completed('Error: Unknown error has occurred.')
+ }
+ } else {
+ // Success
+ if (_.isFunction(completed)) {
+ completed(null, response.data.generatedKey)
+ }
+ }
+ },
+ function error (err) {
+ $log.error('[trudesk:profile:generateL2Auth]')
+ $log.error(err)
+ helpers.UI.showSnackbar('Error: Could not generate new secret! Check Console', true)
if (_.isFunction(completed)) {
- completed(null, response.data.generatedKey)
+ completed(err)
}
}
- },
- function error (err) {
- $log.error('[trudesk:profile:generateL2Auth]')
- $log.error(err)
- helpers.UI.showSnackbar('Error: Could not generate new secret! Check Console', true)
- if (_.isFunction(completed)) {
- completed(err)
- }
- }
- )
+ )
}
function removeL2Auth (completed) {
@@ -266,7 +278,15 @@ define([
}
$http
- .post('/api/v1/users/' + id + '/removel2auth')
+ .post(
+ '/api/v1/users/' + id + '/removel2auth',
+ {},
+ {
+ headers: {
+ 'CSRF-TOKEN': $document[0].querySelector('meta[name="csrf-token"]').getAttribute('content')
+ }
+ }
+ )
.success(function () {
if (_.isFunction(completed)) {
completed()
diff --git a/src/routes/index.js b/src/routes/index.js
index 842de7d61..74d1b0f23 100644
--- a/src/routes/index.js
+++ b/src/routes/index.js
@@ -13,7 +13,7 @@ const express = require('express')
const router = express.Router()
const controllers = require('../controllers')
const path = require('path')
-const winston = require('winston')
+const winston = require('../logger')
const packagejson = require('../../package.json')
function mainRoutes (router, middleware, controllers) {
@@ -226,7 +226,13 @@ function mainRoutes (router, middleware, controllers) {
)
// Accounts
- router.get('/profile', middleware.redirectToLogin, middleware.loadCommonData, controllers.accounts.profile)
+ router.get(
+ '/profile',
+ middleware.redirectToLogin,
+ middleware.csrfCheck,
+ middleware.loadCommonData,
+ controllers.accounts.profile
+ )
router.get('/accounts', middleware.redirectToLogin, middleware.loadCommonData, controllers.accounts.getCustomers)
router.get(
'/accounts/customers',
@@ -328,6 +334,7 @@ function mainRoutes (router, middleware, controllers) {
middleware.redirectToLogin,
middleware.isAdmin,
middleware.loadCommonData,
+ middleware.csrfCheck,
controllers.settings.serverSettings
)
router.get('/settings/legal', middleware.redirectToLogin, middleware.loadCommonData, controllers.settings.legal)
@@ -365,7 +372,7 @@ function mainRoutes (router, middleware, controllers) {
controllers.api.v1.plugins.removePlugin
)
- router.get('/api/v1/admin/restart', middleware.api, middleware.isAdmin, function (req, res) {
+ router.post('/api/v1/admin/restart', middleware.csrfCheck, middleware.api, middleware.isAdmin, function (req, res) {
if (process.env.DISABLE_RESTART) return res.json({ success: true })
const pm2 = require('pm2')
diff --git a/src/views/layout/main.hbs b/src/views/layout/main.hbs
index 09b6a545f..908ae1110 100644
--- a/src/views/layout/main.hbs
+++ b/src/views/layout/main.hbs
@@ -4,6 +4,7 @@