CVE-2022-1752: Unrestricted Upload of File with Dangerous Type (CWE-434)
value: Unrestricted Upload of File with Dangerous Type in GitHub repository polonel/trudesk prior to 1.2.2.
CWE description: The product allows the upload or transfer of dangerous file types that are automatically processed within its environment.
CVSS v3 base score: 8
Published: 2022-05-21T03:15Z
Repository: https://github.com/polonel/trudesk
Language: JavaScript
Files changed: src/controllers/accounts.js, src/controllers/api/v1/routes.js, src/controllers/api/v1/users.js
Commit date: 2022-05-16 23:54:40 -0400
Commit message: refactor(accounts): security enhancements
Diff: diff --git a/src/controllers/accounts.js b/src/controllers/accounts.js
index 7a253a4a4..2353303a7 100644
--- a/src/controllers/accounts.js
+++ b/src/controllers/accounts.js
@@ -17,10 +17,11 @@ const _ = require('lodash')
 const winston = require('../logger')
 const userSchema = require('../models/user')
 const permissions = require('../permissions')
 const emitter = require('../emitter')
 const xss = require('xss')
+const path = require('path')
 
 const accountsController = {}
 
 accountsController.content = {}
 
@@ -460,10 +461,12 @@ accountsController.uploadImage = function (req, res) {
       files: 1,
       fileSize: 1024 * 1024 * 3 // 3mb limit
     }
   })
 
+  const allowedExts = ['.png', '.jpg', '.jpeg', '.gif']
+
   const object = {}
   let error
 
   busboy.on('field', function (fieldname, val) {
     if (fieldname === '_id') object._id = val
@@ -472,11 +475,12 @@ accountsController.uploadImage = function (req, res) {
 
   busboy.on('file', function (name, file, info) {
     const filename = info.filename
     const mimetype = info.mimeType
     const ext = path.extname(filename)
-    if (mimetype.indexOf('image/') === -1 || ext === '.svg') {
+
+    if (!allowedExts.includes(ext)) {
       error = {
         status: 400,
         message: 'Invalid File Type'
       }
 
@@ -484,12 +488,13 @@ accountsController.uploadImage = function (req, res) {
     }
 
     const savePath = path.join(__dirname, '../../public/uploads/users')
     if (!fs.existsSync(savePath)) fs.mkdirSync(savePath)
 
-    object.filePath = path.join(savePath, 'aProfile_' + object.username + path.extname(filename))
     object.filename = 'aProfile_' + object.username + path.extname(filename)
+    object.filename = object.filename.replace('/', '').replace('..', '')
+    object.filePath = path.join(savePath, object.filename)
     object.mimetype = mimetype
 
     file.on('limit', function () {
       error = {
         status: 400,
diff --git a/src/controllers/api/v1/routes.js b/src/controllers/api/v1/routes.js
index 5499c6360..95f98a369 100644
--- a/src/controllers/api/v1/routes.js
+++ b/src/controllers/api/v1/routes.js
@@ -128,11 +128,10 @@ module.exports = function (middleware, router, controllers) {
   router.get('/api/v1/users/notificationCount', apiv1, apiCtrl.users.notificationCount)
   router.get('/api/v1/users/getassignees', apiv1, isAgentOrAdmin, apiCtrl.users.getAssingees)
   router.get('/api/v1/users/:username', apiv1, canUser('accounts:view'), apiCtrl.users.single)
   router.put('/api/v1/users/:username', apiv1, canUser('accounts:update'), apiCtrl.users.update)
   router.get('/api/v1/users/:username/groups', apiv1, apiCtrl.users.getGroups)
-  router.post('/api/v1/users/:username/uploadprofilepic', apiv1, apiCtrl.users.uploadProfilePic)
   router.put('/api/v1/users/:username/updatepreferences', apiv1, apiCtrl.users.updatePreferences)
   router.get('/api/v1/users/:username/enable', apiv1, canUser('accounts:update'), apiCtrl.users.enableUser)
   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)
diff --git a/src/controllers/api/v1/users.js b/src/controllers/api/v1/users.js
index 042b2634a..e6c2683ab 100644
--- a/src/controllers/api/v1/users.js
+++ b/src/controllers/api/v1/users.js
@@ -1078,89 +1078,10 @@ apiUsers.getGroups = function (req, res) {
       return res.json({ success: true, groups: mappedGroups })
     })
   }
 }
 
-apiUsers.uploadProfilePic = function (req, res) {
-  const fs = require('fs')
-  const path = require('path')
-  const Busboy = require('busboy')
-  const busboy = Busboy({
-    headers: req.headers,
-    limits: {
-      files: 1,
-      fileSize: 1024 * 1024 * 3
-    }
-  })
-
-  const object = {}
-  let error
-
-  if (_.isUndefined(req.params.username)) return res.status(400).json({ error: 'Invalid Username' })
-  object.username = req.params.username
-
-  busboy.on('file', function (name, file, info) {
-    const mimetype = info.mimeType
-
-    if (mimetype.indexOf('image/') === -1) {
-      error = {
-        status: 400,
-        message: 'Invalid file type'
-      }
-
-      return file.resume()
-    }
-
-    const savePath = path.join(__dirname, '../../../../public/uploads/users')
-    if (!fs.existsSync(savePath)) fs.mkdirSync(savePath)
-
-    object.filePath = path.join(savePath, 'aProfile_' + object.username + '.jpg')
-    object.filename = 'aProfile_' + object.username + '.jpg'
-    object.mimetype = mimetype
-
-    file.on('limit', function () {
-      error = {
-        status: 400,
-        message: 'File too large'
-      }
-
-      return file.resume()
-    })
-
-    file.pipe(fs.createWriteStream(object.filePath))
-  })
-
-  busboy.on('finish', function () {
-    if (error) return res.status(error.status).send(error.message)
-
-    if (_.isUndefined(object.username) || _.isUndefined(object.filePath) || _.isUndefined(object.filename)) {
-      return res.status(400).send('Invalid Form Data')
-    }
-
-    if (!fs.existsSync(object.filePath)) return res.status(400).send('File failed to save to disk')
-
-    UserSchema.getUserByUsername(object.username, function (err, user) {
-      if (err) return res.status(400).send(err.message)
-
-      user.image = object.filename
-
-      user.save(function (err) {
-        if (err) return res.status(500).send(err.message)
-
-        emitter.emit('trudesk:profileImageUpdate', {
-          userid: user._id,
-          img: user.image
-        })
-
-        return res.json({ success: true, user: stripUserFields(user) })
-      })
-    })
-  })
-
-  req.pipe(busboy)
-}
-
 function stripUserFields (user) {
   user.password = undefined
   user.accessToken = undefined
   user.__v = undefined
   user.tOTPKey = undefined

Vulnerable code:     if (mimetype.indexOf('image/') === -1 || ext === '.svg') {
    object.filePath = path.join(savePath, 'aProfile_' + object.username + path.extname(filename))
  router.post('/api/v1/users/:username/uploadprofilepic', apiv1, apiCtrl.users.uploadProfilePic)
apiUsers.uploadProfilePic = function (req, res) {
  const fs = require('fs')
  const path = require('path')
  const Busboy = require('busboy')
  const busboy = Busboy({
    headers: req.headers,
    limits: {
      files: 1,
      fileSize: 1024 * 1024 * 3
    }
  })

  const object = {}
  let error

  if (_.isUndefined(req.params.username)) return res.status(400).json({ error: 'Invalid Username' })
  object.username = req.params.username

  busboy.on('file', function (name, file, info) {
    const mimetype = info.mimeType

    if (mimetype.indexOf('image/') === -1) {
      error = {
        status: 400,
        message: 'Invalid file type'
      }

      return file.resume()
    }

    const savePath = path.join(__dirname, '../../../../public/uploads/users')
    if (!fs.existsSync(savePath)) fs.mkdirSync(savePath)

    object.filePath = path.join(savePath, 'aProfile_' + object.username + '.jpg')
    object.filename = 'aProfile_' + object.username + '.jpg'
    object.mimetype = mimetype

    file.on('limit', function () {
      error = {
        status: 400,
        message: 'File too large'
      }

      return file.resume()
    })

    file.pipe(fs.createWriteStream(object.filePath))
  })

  busboy.on('finish', function () {
    if (error) return res.status(error.status).send(error.message)

    if (_.isUndefined(object.username) || _.isUndefined(object.filePath) || _.isUndefined(object.filename)) {
      return res.status(400).send('Invalid Form Data')
    }

    if (!fs.existsSync(object.filePath)) return res.status(400).send('File failed to save to disk')

    UserSchema.getUserByUsername(object.username, function (err, user) {
      if (err) return res.status(400).send(err.message)

      user.image = object.filename

      user.save(function (err) {
        if (err) return res.status(500).send(err.message)

        emitter.emit('trudesk:profileImageUpdate', {
          userid: user._id,
          img: user.image
        })

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

  req.pipe(busboy)
}

Fixed code: const path = require('path')
  const allowedExts = ['.png', '.jpg', '.jpeg', '.gif']


    if (!allowedExts.includes(ext)) {
    object.filename = object.filename.replace('/', '').replace('..', '')
    object.filePath = path.join(savePath, object.filename)