]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blobdiff - server/middlewares/validators/users.ts
Don't inject untrusted input
[github/Chocobozzz/PeerTube.git] / server / middlewares / validators / users.ts
index c3a07fccd28cc181094eadd19bfd51bc9bfac79b..50327b6aebf4eb575b0721131c8c56dd2917e653 100644 (file)
@@ -1,10 +1,9 @@
 import express from 'express'
 import { body, param, query } from 'express-validator'
-import { omit } from 'lodash'
 import { Hooks } from '@server/lib/plugins/hooks'
-import { MUserDefault } from '@server/types/models'
+import { forceNumber } from '@shared/core-utils'
 import { HttpStatusCode, UserRegister, UserRight, UserRole } from '@shared/models'
-import { isBooleanValid, isIdValid, toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc'
+import { exists, isBooleanValid, isIdValid, toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc'
 import { isThemeNameValid } from '../../helpers/custom-validators/plugins'
 import {
   isUserAdminFlagsValid,
@@ -31,8 +30,15 @@ import { isThemeRegistered } from '../../lib/plugins/theme-utils'
 import { Redis } from '../../lib/redis'
 import { isSignupAllowed, isSignupAllowedForCurrentIP } from '../../lib/signup'
 import { ActorModel } from '../../models/actor/actor'
-import { UserModel } from '../../models/user/user'
-import { areValidationErrors, doesVideoChannelIdExist, doesVideoExist, isValidVideoIdParam } from './shared'
+import {
+  areValidationErrors,
+  checkUserEmailExist,
+  checkUserIdExist,
+  checkUserNameOrEmailDoesNotAlreadyExist,
+  doesVideoChannelIdExist,
+  doesVideoExist,
+  isValidVideoIdParam
+} from './shared'
 
 const usersListValidator = [
   query('blocked')
@@ -41,8 +47,6 @@ const usersListValidator = [
     .isBoolean().withMessage('Should be a valid blocked boolena'),
 
   (req: express.Request, res: express.Response, next: express.NextFunction) => {
-    logger.debug('Checking usersList parameters', { parameters: req.query })
-
     if (areValidationErrors(req, res)) return
 
     return next()
@@ -76,9 +80,7 @@ const usersAddValidator = [
     .custom(isUserAdminFlagsValid),
 
   async (req: express.Request, res: express.Response, next: express.NextFunction) => {
-    logger.debug('Checking usersAdd parameters', { parameters: omit(req.body, 'password') })
-
-    if (areValidationErrors(req, res)) return
+    if (areValidationErrors(req, res, { omitBodyLog: true })) return
     if (!await checkUserNameOrEmailDoesNotAlreadyExist(req.body.username, req.body.email, res)) return
 
     const authUser = res.locals.oauth.token.User
@@ -126,9 +128,7 @@ const usersRegisterValidator = [
     .custom(isVideoChannelDisplayNameValid),
 
   async (req: express.Request, res: express.Response, next: express.NextFunction) => {
-    logger.debug('Checking usersRegister parameters', { parameters: omit(req.body, 'password') })
-
-    if (areValidationErrors(req, res)) return
+    if (areValidationErrors(req, res, { omitBodyLog: true })) return
     if (!await checkUserNameOrEmailDoesNotAlreadyExist(req.body.username, req.body.email, res)) return
 
     const body: UserRegister = req.body
@@ -159,8 +159,6 @@ const usersRemoveValidator = [
     .custom(isIdValid),
 
   async (req: express.Request, res: express.Response, next: express.NextFunction) => {
-    logger.debug('Checking usersRemove parameters', { parameters: req.params })
-
     if (areValidationErrors(req, res)) return
     if (!await checkUserIdExist(req.params.id, res)) return
 
@@ -181,8 +179,6 @@ const usersBlockingValidator = [
     .custom(isUserBlockedReasonValid),
 
   async (req: express.Request, res: express.Response, next: express.NextFunction) => {
-    logger.debug('Checking usersBlocking parameters', { parameters: req.params })
-
     if (areValidationErrors(req, res)) return
     if (!await checkUserIdExist(req.params.id, res)) return
 
@@ -236,9 +232,7 @@ const usersUpdateValidator = [
     .custom(isUserAdminFlagsValid),
 
   async (req: express.Request, res: express.Response, next: express.NextFunction) => {
-    logger.debug('Checking usersUpdate parameters', { parameters: req.body })
-
-    if (areValidationErrors(req, res)) return
+    if (areValidationErrors(req, res, { omitBodyLog: true })) return
     if (!await checkUserIdExist(req.params.id, res)) return
 
     const user = res.locals.user
@@ -300,8 +294,6 @@ const usersUpdateMeValidator = [
     .custom(v => isUserAutoPlayNextVideoValid(v)).withMessage('Should have a valid autoPlayNextVideo boolean'),
 
   async (req: express.Request, res: express.Response, next: express.NextFunction) => {
-    logger.debug('Checking usersUpdateMe parameters', { parameters: omit(req.body, 'password') })
-
     const user = res.locals.oauth.token.User
 
     if (req.body.password || req.body.email) {
@@ -321,7 +313,7 @@ const usersUpdateMeValidator = [
       }
     }
 
-    if (areValidationErrors(req, res)) return
+    if (areValidationErrors(req, res, { omitBodyLog: true })) return
 
     return next()
   }
@@ -335,8 +327,6 @@ const usersGetValidator = [
     .isBoolean().withMessage('Should have a valid withStats boolean'),
 
   async (req: express.Request, res: express.Response, next: express.NextFunction) => {
-    logger.debug('Checking usersGet parameters', { parameters: req.params })
-
     if (areValidationErrors(req, res)) return
     if (!await checkUserIdExist(req.params.id, res, req.query.withStats)) return
 
@@ -348,8 +338,6 @@ const usersVideoRatingValidator = [
   isValidVideoIdParam('videoId'),
 
   async (req: express.Request, res: express.Response, next: express.NextFunction) => {
-    logger.debug('Checking usersVideoRating parameters', { parameters: req.params })
-
     if (areValidationErrors(req, res)) return
     if (!await doesVideoExist(req.params.videoId, res, 'id')) return
 
@@ -369,8 +357,6 @@ const usersVideosValidator = [
     .custom(isIdValid),
 
   async (req: express.Request, res: express.Response, next: express.NextFunction) => {
-    logger.debug('Checking usersVideosValidator parameters', { parameters: req.query })
-
     if (areValidationErrors(req, res)) return
 
     if (req.query.channelId && !await doesVideoChannelIdExist(req.query.channelId, res)) return
@@ -423,8 +409,6 @@ const usersAskResetPasswordValidator = [
     .isEmail(),
 
   async (req: express.Request, res: express.Response, next: express.NextFunction) => {
-    logger.debug('Checking usersAskResetPassword parameters', { parameters: req.body })
-
     if (areValidationErrors(req, res)) return
 
     const exists = await checkUserEmailExist(req.body.email, res, false)
@@ -434,6 +418,13 @@ const usersAskResetPasswordValidator = [
       return res.status(HttpStatusCode.NO_CONTENT_204).end()
     }
 
+    if (res.locals.user.pluginAuth) {
+      return res.fail({
+        status: HttpStatusCode.CONFLICT_409,
+        message: 'Cannot recover password of a user that uses a plugin authentication.'
+      })
+    }
+
     return next()
   }
 ]
@@ -447,13 +438,11 @@ const usersResetPasswordValidator = [
     .custom(isUserPasswordValid),
 
   async (req: express.Request, res: express.Response, next: express.NextFunction) => {
-    logger.debug('Checking usersResetPassword parameters', { parameters: req.params })
-
     if (areValidationErrors(req, res)) return
     if (!await checkUserIdExist(req.params.id, res)) return
 
     const user = res.locals.user
-    const redisVerificationString = await Redis.Instance.getResetPasswordLink(user.id)
+    const redisVerificationString = await Redis.Instance.getResetPasswordVerificationString(user.id)
 
     if (redisVerificationString !== req.body.verificationString) {
       return res.fail({
@@ -470,9 +459,8 @@ const usersAskSendVerifyEmailValidator = [
   body('email').isEmail().not().isEmpty().withMessage('Should have a valid email'),
 
   async (req: express.Request, res: express.Response, next: express.NextFunction) => {
-    logger.debug('Checking askUsersSendVerifyEmail parameters', { parameters: req.body })
-
     if (areValidationErrors(req, res)) return
+
     const exists = await checkUserEmailExist(req.body.email, res, false)
     if (!exists) {
       logger.debug('User with email %s does not exist (asking verify email).', req.body.email)
@@ -480,6 +468,13 @@ const usersAskSendVerifyEmailValidator = [
       return res.status(HttpStatusCode.NO_CONTENT_204).end()
     }
 
+    if (res.locals.user.pluginAuth) {
+      return res.fail({
+        status: HttpStatusCode.CONFLICT_409,
+        message: 'Cannot ask verification email of a user that uses a plugin authentication.'
+      })
+    }
+
     return next()
   }
 ]
@@ -495,8 +490,6 @@ const usersVerifyEmailValidator = [
     .customSanitizer(toBooleanOrNull),
 
   async (req: express.Request, res: express.Response, next: express.NextFunction) => {
-    logger.debug('Checking usersVerifyEmail parameters', { parameters: req.params })
-
     if (areValidationErrors(req, res)) return
     if (!await checkUserIdExist(req.params.id, res)) return
 
@@ -514,8 +507,45 @@ const usersVerifyEmailValidator = [
   }
 ]
 
+const usersCheckCurrentPasswordFactory = (targetUserIdGetter: (req: express.Request) => number | string) => {
+  return [
+    body('currentPassword').optional().custom(exists),
+
+    async (req: express.Request, res: express.Response, next: express.NextFunction) => {
+      if (areValidationErrors(req, res)) return
+
+      const user = res.locals.oauth.token.User
+      const isAdminOrModerator = user.role === UserRole.ADMINISTRATOR || user.role === UserRole.MODERATOR
+      const targetUserId = forceNumber(targetUserIdGetter(req))
+
+      // Admin/moderator action on another user, skip the password check
+      if (isAdminOrModerator && targetUserId !== user.id) {
+        return next()
+      }
+
+      if (!req.body.currentPassword) {
+        return res.fail({
+          status: HttpStatusCode.BAD_REQUEST_400,
+          message: 'currentPassword is missing'
+        })
+      }
+
+      if (await user.isPasswordMatch(req.body.currentPassword) !== true) {
+        return res.fail({
+          status: HttpStatusCode.FORBIDDEN_403,
+          message: 'currentPassword is invalid.'
+        })
+      }
+
+      return next()
+    }
+  ]
+}
+
 const userAutocompleteValidator = [
-  param('search').isString().not().isEmpty().withMessage('Should have a search parameter')
+  param('search')
+    .isString()
+    .not().isEmpty()
 ]
 
 const ensureAuthUserOwnsAccountValidator = [
@@ -533,13 +563,14 @@ const ensureAuthUserOwnsAccountValidator = [
   }
 ]
 
-const ensureCanManageChannel = [
+const ensureCanManageChannelOrAccount = [
   (req: express.Request, res: express.Response, next: express.NextFunction) => {
     const user = res.locals.oauth.token.user
-    const isUserOwner = res.locals.videoChannel.Account.userId === user.id
+    const account = res.locals.videoChannel?.Account ?? res.locals.account
+    const isUserOwner = account.userId === user.id
 
     if (!isUserOwner && user.hasRight(UserRight.MANAGE_ANY_VIDEO_CHANNEL) === false) {
-      const message = `User ${user.username} does not have right to manage channel ${req.params.nameWithHost}.`
+      const message = `User ${user.username} does not have right this channel or account.`
 
       return res.fail({
         status: HttpStatusCode.FORBIDDEN_403,
@@ -551,7 +582,7 @@ const ensureCanManageChannel = [
   }
 ]
 
-const ensureCanManageUser = [
+const ensureCanModerateUser = [
   (req: express.Request, res: express.Response, next: express.NextFunction) => {
     const authUser = res.locals.oauth.token.User
     const onUser = res.locals.user
@@ -561,7 +592,7 @@ const ensureCanManageUser = [
 
     return res.fail({
       status: HttpStatusCode.FORBIDDEN_403,
-      message: 'A moderator can only manager users.'
+      message: 'A moderator can only manage users.'
     })
   }
 ]
@@ -578,6 +609,7 @@ export {
   usersUpdateValidator,
   usersUpdateMeValidator,
   usersVideoRatingValidator,
+  usersCheckCurrentPasswordFactory,
   ensureUserRegistrationAllowed,
   ensureUserRegistrationAllowedForIP,
   usersGetValidator,
@@ -588,58 +620,6 @@ export {
   usersVerifyEmailValidator,
   userAutocompleteValidator,
   ensureAuthUserOwnsAccountValidator,
-  ensureCanManageUser,
-  ensureCanManageChannel
-}
-
-// ---------------------------------------------------------------------------
-
-function checkUserIdExist (idArg: number | string, res: express.Response, withStats = false) {
-  const id = parseInt(idArg + '', 10)
-  return checkUserExist(() => UserModel.loadByIdWithChannels(id, withStats), res)
-}
-
-function checkUserEmailExist (email: string, res: express.Response, abortResponse = true) {
-  return checkUserExist(() => UserModel.loadByEmail(email), res, abortResponse)
-}
-
-async function checkUserNameOrEmailDoesNotAlreadyExist (username: string, email: string, res: express.Response) {
-  const user = await UserModel.loadByUsernameOrEmail(username, email)
-
-  if (user) {
-    res.fail({
-      status: HttpStatusCode.CONFLICT_409,
-      message: 'User with this username or email already exists.'
-    })
-    return false
-  }
-
-  const actor = await ActorModel.loadLocalByName(username)
-  if (actor) {
-    res.fail({
-      status: HttpStatusCode.CONFLICT_409,
-      message: 'Another actor (account/channel) with this name on this instance already exists or has already existed.'
-    })
-    return false
-  }
-
-  return true
-}
-
-async function checkUserExist (finder: () => Promise<MUserDefault>, res: express.Response, abortResponse = true) {
-  const user = await finder()
-
-  if (!user) {
-    if (abortResponse === true) {
-      res.fail({
-        status: HttpStatusCode.NOT_FOUND_404,
-        message: 'User not found'
-      })
-    }
-
-    return false
-  }
-
-  res.locals.user = user
-  return true
+  ensureCanModerateUser,
+  ensureCanManageChannelOrAccount
 }