CVE-2022-2023: Improper Privilege Management (CWE-269)
value: Incorrect Use of Privileged APIs in GitHub repository polonel/trudesk prior to 1.2.4.
CWE description: The product does not properly assign, modify, track, or check privileges for an actor, creating an unintended sphere of control for that actor.
CVSS v3 base score: 9.8
Published: 2022-06-20T04:15Z
Repository: https://github.com/polonel/trudesk
Language: JavaScript
Files changed: src/controllers/api/v1/routes.js, src/controllers/api/v1/users.js, src/public/js/angularjs/controllers/profile.js
Commit date: 2022-06-07 23:57:07 -0400
Commit message: fix(accounts): security enhancement
Diff: diff --git a/src/controllers/api/v1/routes.js b/src/controllers/api/v1/routes.js
index ed9dc839a..ba2a45f53 100644
--- a/src/controllers/api/v1/routes.js
+++ b/src/controllers/api/v1/routes.js
@@ -120,10 +120,11 @@ module.exports = function (middleware, router, controllers) {
   router.get('/api/v1/groups/:id', apiv1, apiCtrl.groups.getSingleGroup)
   router.put('/api/v1/groups/:id', apiv1, canUser('groups:update'), apiCtrl.groups.updateGroup)
   router.delete('/api/v1/groups/:id', apiv1, canUser('groups:delete'), apiCtrl.groups.deleteGroup)
 
   // Users
+  router.put('/api/v1/profile', apiv1, apiCtrl.users.profileUpdate)
   router.get('/api/v1/users', apiv1, canUser('accounts:view'), apiCtrl.users.getWithLimit)
   router.post('/api/v1/users/create', apiv1, canUser('accounts:create'), apiCtrl.users.create)
   router.get('/api/v1/users/notifications', apiv1, apiCtrl.users.getNotifications)
   router.get('/api/v1/users/notificationCount', apiv1, apiCtrl.users.notificationCount)
   router.get('/api/v1/users/getassignees', apiv1, isAgentOrAdmin, apiCtrl.users.getAssingees)
diff --git a/src/controllers/api/v1/users.js b/src/controllers/api/v1/users.js
index 11f2aae64..3fa2bb168 100644
--- a/src/controllers/api/v1/users.js
+++ b/src/controllers/api/v1/users.js
@@ -382,10 +382,108 @@ apiUsers.createPublicAccount = function (req, res) {
       })
     }
   )
 }
 
+apiUsers.profileUpdate = function (req, res) {
+  if (!req.user) return res.status(400).json({ success: false, error: 'Invalid Post Data' })
+  const username = req.user.username
+  if (_.isNull(username) || _.isUndefined(username))
+    return res.status(400).json({ success: false, error: 'Invalid Post Data' })
+
+  const data = req.body
+  let passwordUpdated = false
+
+  const obj = {
+    fullname: data.aFullname,
+    title: data.aTitle,
+    password: data.aPassword,
+    passconfirm: data.aPassConfirm,
+    email: data.aEmail
+  }
+
+  let passwordComplexityEnabled = true
+
+  async.series(
+    {
+      settings: function (done) {
+        const SettingUtil = require('../../../settings/settingsUtil')
+        SettingUtil.getSettings(function (err, content) {
+          if (err) return done(err)
+
+          const settings = content.data.settings
+          passwordComplexityEnabled = settings.accountsPasswordComplexity.value
+
+          return done()
+        })
+      },
+      user: function (done) {
+        UserSchema.getUserByUsername(username, function (err, user) {
+          if (err) return done(err)
+          if (!user) return done('Invalid User Object')
+
+          obj._id = user._id
+
+          if (
+            !_.isUndefined(obj.password) &&
+            !_.isEmpty(obj.password) &&
+            !_.isUndefined(obj.passconfirm) &&
+            !_.isEmpty(obj.passconfirm)
+          ) {
+            if (obj.password === obj.passconfirm) {
+              if (passwordComplexityEnabled) {
+                // check Password Complexity
+                const passwordComplexity = require('../../../settings/passwordComplexity')
+                if (!passwordComplexity.validate(obj.password)) return done('Password does not meet requirements')
+              }
+
+              user.password = obj.password
+              passwordUpdated = true
+            }
+          }
+
+          if (!_.isUndefined(obj.fullname) && obj.fullname.length > 0) user.fullname = obj.fullname
+          if (!_.isUndefined(obj.email) && obj.email.length > 0) user.email = obj.email
+          if (!_.isUndefined(obj.title) && obj.title.length > 0) user.title = obj.title
+
+          user.save(function (err, nUser) {
+            if (err) return done(err)
+
+            nUser.populate('role', function (err, populatedUser) {
+              if (err) return done(err)
+              const resUser = stripUserFields(populatedUser)
+
+              return done(null, resUser)
+            })
+          })
+        })
+      },
+      groups: function (done) {
+        groupSchema.getAllGroupsOfUser(obj._id, done)
+      }
+    },
+    async function (err, results) {
+      if (err) {
+        winston.debug(err)
+        return res.status(400).json({ success: false, error: err })
+      }
+
+      const user = results.user.toJSON()
+      user.groups = results.groups.map(function (g) {
+        return { _id: g._id, name: g.name }
+      })
+
+      if (passwordUpdated) {
+        const Session = require('../../../models/session')
+        await Session.destroy(user._id)
+      }
+
+      return res.json({ success: true, user: user })
+    }
+  )
+}
+
 /**
  * @api {put} /api/v1/users/:username Update User
  * @apiName updateUser
  * @apiDescription Updates a single user.
  * @apiVersion 0.1.7
diff --git a/src/public/js/angularjs/controllers/profile.js b/src/public/js/angularjs/controllers/profile.js
index 704ec532c..898c08848 100644
--- a/src/public/js/angularjs/controllers/profile.js
+++ b/src/public/js/angularjs/controllers/profile.js
@@ -72,11 +72,11 @@ define([
           helpers.UI.showSnackbar('Form data invalid.', true)
           return false
         }
 
         $http
-          .put('/api/v1/users/' + data.username, {
+          .put('/api/v1/profile/', {
             aId: id,
             aFullname: data.fullname,
             aPass: data.password,
             aPassConfirm: data.cPassword,
             aEmail: data.email,

Vulnerable code:           .put('/api/v1/users/' + data.username, {
Fixed code:   router.put('/api/v1/profile', apiv1, apiCtrl.users.profileUpdate)
apiUsers.profileUpdate = function (req, res) {
  if (!req.user) return res.status(400).json({ success: false, error: 'Invalid Post Data' })
  const username = req.user.username
  if (_.isNull(username) || _.isUndefined(username))
    return res.status(400).json({ success: false, error: 'Invalid Post Data' })

  const data = req.body
  let passwordUpdated = false

  const obj = {
    fullname: data.aFullname,
    title: data.aTitle,
    password: data.aPassword,
    passconfirm: data.aPassConfirm,
    email: data.aEmail
  }

  let passwordComplexityEnabled = true

  async.series(
    {
      settings: function (done) {
        const SettingUtil = require('../../../settings/settingsUtil')
        SettingUtil.getSettings(function (err, content) {
          if (err) return done(err)

          const settings = content.data.settings
          passwordComplexityEnabled = settings.accountsPasswordComplexity.value

          return done()
        })
      },
      user: function (done) {
        UserSchema.getUserByUsername(username, function (err, user) {
          if (err) return done(err)
          if (!user) return done('Invalid User Object')

          obj._id = user._id

          if (
            !_.isUndefined(obj.password) &&
            !_.isEmpty(obj.password) &&
            !_.isUndefined(obj.passconfirm) &&
            !_.isEmpty(obj.passconfirm)
          ) {
            if (obj.password === obj.passconfirm) {
              if (passwordComplexityEnabled) {
                // check Password Complexity
                const passwordComplexity = require('../../../settings/passwordComplexity')
                if (!passwordComplexity.validate(obj.password)) return done('Password does not meet requirements')
              }

              user.password = obj.password
              passwordUpdated = true
            }
          }

          if (!_.isUndefined(obj.fullname) && obj.fullname.length > 0) user.fullname = obj.fullname
          if (!_.isUndefined(obj.email) && obj.email.length > 0) user.email = obj.email
          if (!_.isUndefined(obj.title) && obj.title.length > 0) user.title = obj.title

          user.save(function (err, nUser) {
            if (err) return done(err)

            nUser.populate('role', function (err, populatedUser) {
              if (err) return done(err)
              const resUser = stripUserFields(populatedUser)

              return done(null, resUser)
            })
          })
        })
      },
      groups: function (done) {
        groupSchema.getAllGroupsOfUser(obj._id, done)
      }
    },
    async function (err, results) {
      if (err) {
        winston.debug(err)
        return res.status(400).json({ success: false, error: err })
      }

      const user = results.user.toJSON()
      user.groups = results.groups.map(function (g) {
        return { _id: g._id, name: g.name }
      })

      if (passwordUpdated) {
        const Session = require('../../../models/session')
        await Session.destroy(user._id)
      }

      return res.json({ success: true, user: user })
    }
  )
}

          .put('/api/v1/profile/', {