Skip to content

Commit

Permalink
refactor(accounts): security enhancement
Browse files Browse the repository at this point in the history
  • Loading branch information
polonel committed May 17, 2022
1 parent e2db47f commit 2512b8d
Show file tree
Hide file tree
Showing 10 changed files with 418 additions and 35 deletions.
3 changes: 3 additions & 0 deletions package.json
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
13 changes: 11 additions & 2 deletions src/client/containers/Settings/Server/index.jsx
Expand Up @@ -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)
})
Expand Down
4 changes: 2 additions & 2 deletions src/controllers/api/v1/routes.js
Expand Up @@ -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)
Expand Down
314 changes: 314 additions & 0 deletions 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
3 changes: 3 additions & 0 deletions src/middleware/index.js
Expand Up @@ -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) {
Expand Down

0 comments on commit 2512b8d

Please sign in to comment.