CVE-2021-46561 (JavaScript)

controller/org.controller/org.controller.js in the CVE Services API 1.1.1 before 5c50baf3bda28133a3bc90b854765a64fb538304 allows an organizational administrator to transfer a user account to an arbitrary new organization, and thereby achieve unintended access within the context of that new organization.

Weakness:
CWE-863: name: Incorrect Authorization
description: The product performs an authorization check when an actor attempts to access a resource or perform an action, but it does not correctly perform the check.

Repository: https://github.com/CVEProject/cve-services

Vulnerable code:
file_path: src/controller/org.controller/org.controller.js
snippet:     Object.keys(req.ctx.query).forEach(k => {
      const key = k.toLowerCase()

      if (key === 'new_username') {
        newUser.username = req.ctx.query.new_username
      } else if (key === 'org_shortname') {
        newOrgShortName = req.ctx.query.org_shortname
        changesRequirePrivilegedRole = true
      } else if (key === 'name.first') {
        newUser.name.first = req.ctx.query['name.first']
      } else if (key === 'name.last') {
        newUser.name.last = req.ctx.query['name.last']
      } else if (key === 'name.middle') {
        newUser.name.middle = req.ctx.query['name.middle']
      } else if (key === 'name.suffix') {
        newUser.name.suffix = req.ctx.query['name.suffix']
      } else if (key === 'name.surname') {
        newUser.name.surname = req.ctx.query['name.surname']
      } else if (key === 'active') {
        newUser.active = req.ctx.query.active
        changesRequirePrivilegedRole = true
      } else if (key === 'active_roles.add') {
        if (Array.isArray(req.ctx.query['active_roles.add'])) {
          req.ctx.query['active_roles.add'].forEach(r => {
            addRoles.push(r)
          })
          changesRequirePrivilegedRole = true
        }
      } else if (key === 'active_roles.remove') {
        if (Array.isArray(req.ctx.query['active_roles.remove'])) {
          req.ctx.query['active_roles.remove'].forEach(r => {
            removeRoles.push(r)
          })
          changesRequirePrivilegedRole = true
        }
      }
    })

Fixed code:
file_path: src/controller/org.controller/error.js
snippet:   notAllowedToChangeOrganization () {
    const err = {}
    err.error = 'NOT_ALLOWED_TO_CHANGE_ORGANIZATION'
    err.message = 'Only the Secretariat can change the organization for a user.'
    return err
  }


file_path: src/controller/org.controller/org.controller.js
snippet: async function updateUser (req, res, next) {
  try {
    const requesterShortName = req.ctx.org
    const requesterUsername = req.ctx.user
    const username = req.ctx.params.username
    const shortName = req.ctx.params.shortname
    const newUser = new User()
    let newOrgShortName
    let changesRequirePrivilegedRole // Set variable to true if protected fields are being modified
    const removeRoles = []
    const addRoles = []
    const userRepo = req.ctx.repositories.getUserRepository()
    const orgRepo = req.ctx.repositories.getOrgRepository()
    const orgUUID = await orgRepo.getOrgUUID(shortName)
    const isSecretariat = await orgRepo.isSecretariat(requesterShortName)
    const isAdmin = await userRepo.isAdmin(requesterUsername, requesterShortName) // Check if requester is Admin of the designated user's org

    if (!orgUUID) {
      logger.info({ uuid: req.ctx.uuid, message: 'The user could not be updated because ' + shortName + ' organization does not exist.' })
      return res.status(404).json(error.orgDneParam(shortName))
    }

    const user = await userRepo.findOneByUserNameAndOrgUUID(username, orgUUID)
    if (!user) {
      logger.info({ uuid: req.ctx.uuid, message: 'The user could not be updated because ' + username + ' does not exist for ' + shortName + ' organization.' })
      return res.status(404).json(error.userDne(username))
    }

    // check if the user is not the requester or if the requester is not a secretariat
    if ((shortName !== requesterShortName || username !== requesterUsername) && !isSecretariat) {
      // check if the requester is not and admin; if admin, the requester must be from the same org as the user
      if (!isAdmin || (isAdmin && shortName !== requesterShortName)) {
        logger.info({ uuid: req.ctx.uuid, message: 'The user can only be updated by the Secretariat, an Org admin or if the requester is the user.' })
        return res.status(403).json(error.notSameUserOrSecretariat())
      }
    }

    Object.keys(req.ctx.query).forEach(k => {
      const key = k.toLowerCase()

      if (key === 'new_username') {
        newUser.username = req.ctx.query.new_username
      } else if (key === 'org_shortname') {
        newOrgShortName = req.ctx.query.org_shortname
        changesRequirePrivilegedRole = true
        if (!isSecretariat) {
          logger.info({ uuid: req.ctx.uuid, message: 'The user could not be updated because ' + requesterUsername + ' is an Org Admin and tried to reassign the organization.' })
          return res.status(403).json(error.notAllowedToChangeOrganization())
        }
      } else if (key === 'name.first') {
        newUser.name.first = req.ctx.query['name.first']
      } else if (key === 'name.last') {
        newUser.name.last = req.ctx.query['name.last']
      } else if (key === 'name.middle') {
        newUser.name.middle = req.ctx.query['name.middle']
      } else if (key === 'name.suffix') {
        newUser.name.suffix = req.ctx.query['name.suffix']
      } else if (key === 'name.surname') {
        newUser.name.surname = req.ctx.query['name.surname']
      } else if (key === 'active') {
        newUser.active = req.ctx.query.active
        changesRequirePrivilegedRole = true
      } else if (key === 'active_roles.add') {
        if (Array.isArray(req.ctx.query['active_roles.add'])) {
          req.ctx.query['active_roles.add'].forEach(r => {
            addRoles.push(r)
          })
          changesRequirePrivilegedRole = true
        }
      } else if (key === 'active_roles.remove') {
        if (Array.isArray(req.ctx.query['active_roles.remove'])) {
          req.ctx.query['active_roles.remove'].forEach(r => {
            removeRoles.push(r)
          })
          changesRequirePrivilegedRole = true
        }
      }
    })

    // updating user's roles and org_uuid is only allowed for secretariats and org admins
    if (changesRequirePrivilegedRole && !(isAdmin || isSecretariat)) {
      logger.info({ uuid: req.ctx.uuid, message: 'The user could not be updated because ' + requesterUsername + ' user is not Org Admin or Secretariat to modify these fields.' })
      return res.status(403).json(error.notOrgAdminOrSecretariat())
    }

    // check if the new org exist
    if (newOrgShortName) {
      newUser.org_UUID = await orgRepo.getOrgUUID(newOrgShortName)

      if (!newUser.org_UUID) {
        logger.info({ uuid: req.ctx.uuid, message: 'The user could not be updated because ' + newOrgShortName + ' organization does not exist.' })
        return res.status(404).json(error.orgDne(newOrgShortName))
      }
    }

    let agt = setAggregateUserObj({ username: username, org_UUID: orgUUID })

    // check if org has user of same username already
    if (newUser.username && newUser.org_UUID) {
      agt = setAggregateUserObj({ username: newUser.username, org_UUID: newUser.org_UUID })
      const duplicateUsers = await userRepo.find({ org_UUID: newUser.org_UUID, username: newUser.username })
      if (duplicateUsers.length) {
        logger.info({ uuid: req.ctx.uuid, message: 'The user could not be updated because ' + newOrgShortName + ' organization contains a user with the same username.' })
        return res.status(403).json(error.duplicateUsername(newOrgShortName, newUser.username))
      }
    } else if (newUser.username) {
      agt = setAggregateUserObj({ username: newUser.username, org_UUID: orgUUID })
      const duplicateUsers = await userRepo.find({ org_UUID: orgUUID, username: newUser.username })
      if (duplicateUsers.length) {
        logger.info({ uuid: req.ctx.uuid, message: 'The user could not be updated because ' + shortName + ' organization contains a user with the same username.' })
        return res.status(403).json(error.duplicateUsername(shortName, newUser.username))
      }
    } else if (newUser.org_UUID) {
      agt = setAggregateUserObj({ username: username, org_UUID: newUser.org_UUID })
      const duplicateUsers = await userRepo.find({ org_UUID: newUser.org_UUID, username: username })
      if (duplicateUsers.length) {
        logger.info({ uuid: req.ctx.uuid, message: 'The user could not be updated because ' + newOrgShortName + ' organization contains a user with the same username.' })
        return res.status(403).json(error.duplicateUsername(newOrgShortName, username))
      }
    }

    // updating the user's roles
    const roles = user.authority.active_roles

    // adding roles
    addRoles.forEach(role => {
      if (!roles.includes(role)) {
        roles.push(role)
      }
    })

    // removing roles
    removeRoles.forEach(role => {
      const index = roles.indexOf(role)

      if (index > -1) {
        roles.splice(index, 1)
      }
    })

    newUser.authority.active_roles = roles

    let result = await userRepo.updateByUserNameAndOrgUUID(username, orgUUID, newUser)
    if (result.n === 0) {
      logger.info({ uuid: req.ctx.uuid, message: 'The user could not be updated because ' + username + ' does not exist for ' + shortName + ' organization.' })
      return res.status(404).json(error.userDne(username))
    }

    result = await userRepo.aggregate(agt)
    result = result.length > 0 ? result[0] : null

    const responseMessage = {
      message: username + ' was successfully updated.',
      updated: result
    }

    const payload = {
      action: 'update_user',
      change: username + ' was successfully updated.',
      req_UUID: req.ctx.uuid,
      org_UUID: await orgRepo.getOrgUUID(req.ctx.org),
      user: result
    }
    payload.user_UUID = await userRepo.getUserUUID(req.ctx.user, payload.org_UUID)
    logger.info(JSON.stringify(payload))
    return res.status(200).json(responseMessage)
  } catch (err) {
    next(err)
  }
}