]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/middlewares/validators/users.ts
Don't inject untrusted input
[github/Chocobozzz/PeerTube.git] / server / middlewares / validators / users.ts
1 import express from 'express'
2 import { body, param, query } from 'express-validator'
3 import { Hooks } from '@server/lib/plugins/hooks'
4 import { forceNumber } from '@shared/core-utils'
5 import { HttpStatusCode, UserRegister, UserRight, UserRole } from '@shared/models'
6 import { exists, isBooleanValid, isIdValid, toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc'
7 import { isThemeNameValid } from '../../helpers/custom-validators/plugins'
8 import {
9 isUserAdminFlagsValid,
10 isUserAutoPlayNextVideoValid,
11 isUserAutoPlayVideoValid,
12 isUserBlockedReasonValid,
13 isUserDescriptionValid,
14 isUserDisplayNameValid,
15 isUserNoModal,
16 isUserNSFWPolicyValid,
17 isUserP2PEnabledValid,
18 isUserPasswordValid,
19 isUserPasswordValidOrEmpty,
20 isUserRoleValid,
21 isUserUsernameValid,
22 isUserVideoLanguages,
23 isUserVideoQuotaDailyValid,
24 isUserVideoQuotaValid,
25 isUserVideosHistoryEnabledValid
26 } from '../../helpers/custom-validators/users'
27 import { isVideoChannelDisplayNameValid, isVideoChannelUsernameValid } from '../../helpers/custom-validators/video-channels'
28 import { logger } from '../../helpers/logger'
29 import { isThemeRegistered } from '../../lib/plugins/theme-utils'
30 import { Redis } from '../../lib/redis'
31 import { isSignupAllowed, isSignupAllowedForCurrentIP } from '../../lib/signup'
32 import { ActorModel } from '../../models/actor/actor'
33 import {
34 areValidationErrors,
35 checkUserEmailExist,
36 checkUserIdExist,
37 checkUserNameOrEmailDoesNotAlreadyExist,
38 doesVideoChannelIdExist,
39 doesVideoExist,
40 isValidVideoIdParam
41 } from './shared'
42
43 const usersListValidator = [
44 query('blocked')
45 .optional()
46 .customSanitizer(toBooleanOrNull)
47 .isBoolean().withMessage('Should be a valid blocked boolena'),
48
49 (req: express.Request, res: express.Response, next: express.NextFunction) => {
50 if (areValidationErrors(req, res)) return
51
52 return next()
53 }
54 ]
55
56 const usersAddValidator = [
57 body('username')
58 .custom(isUserUsernameValid)
59 .withMessage('Should have a valid username (lowercase alphanumeric characters)'),
60 body('password')
61 .custom(isUserPasswordValidOrEmpty),
62 body('email')
63 .isEmail(),
64
65 body('channelName')
66 .optional()
67 .custom(isVideoChannelUsernameValid),
68
69 body('videoQuota')
70 .custom(isUserVideoQuotaValid),
71 body('videoQuotaDaily')
72 .custom(isUserVideoQuotaDailyValid),
73
74 body('role')
75 .customSanitizer(toIntOrNull)
76 .custom(isUserRoleValid),
77
78 body('adminFlags')
79 .optional()
80 .custom(isUserAdminFlagsValid),
81
82 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
83 if (areValidationErrors(req, res, { omitBodyLog: true })) return
84 if (!await checkUserNameOrEmailDoesNotAlreadyExist(req.body.username, req.body.email, res)) return
85
86 const authUser = res.locals.oauth.token.User
87 if (authUser.role !== UserRole.ADMINISTRATOR && req.body.role !== UserRole.USER) {
88 return res.fail({
89 status: HttpStatusCode.FORBIDDEN_403,
90 message: 'You can only create users (and not administrators or moderators)'
91 })
92 }
93
94 if (req.body.channelName) {
95 if (req.body.channelName === req.body.username) {
96 return res.fail({ message: 'Channel name cannot be the same as user username.' })
97 }
98
99 const existing = await ActorModel.loadLocalByName(req.body.channelName)
100 if (existing) {
101 return res.fail({
102 status: HttpStatusCode.CONFLICT_409,
103 message: `Channel with name ${req.body.channelName} already exists.`
104 })
105 }
106 }
107
108 return next()
109 }
110 ]
111
112 const usersRegisterValidator = [
113 body('username')
114 .custom(isUserUsernameValid),
115 body('password')
116 .custom(isUserPasswordValid),
117 body('email')
118 .isEmail(),
119 body('displayName')
120 .optional()
121 .custom(isUserDisplayNameValid),
122
123 body('channel.name')
124 .optional()
125 .custom(isVideoChannelUsernameValid),
126 body('channel.displayName')
127 .optional()
128 .custom(isVideoChannelDisplayNameValid),
129
130 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
131 if (areValidationErrors(req, res, { omitBodyLog: true })) return
132 if (!await checkUserNameOrEmailDoesNotAlreadyExist(req.body.username, req.body.email, res)) return
133
134 const body: UserRegister = req.body
135 if (body.channel) {
136 if (!body.channel.name || !body.channel.displayName) {
137 return res.fail({ message: 'Channel is optional but if you specify it, channel.name and channel.displayName are required.' })
138 }
139
140 if (body.channel.name === body.username) {
141 return res.fail({ message: 'Channel name cannot be the same as user username.' })
142 }
143
144 const existing = await ActorModel.loadLocalByName(body.channel.name)
145 if (existing) {
146 return res.fail({
147 status: HttpStatusCode.CONFLICT_409,
148 message: `Channel with name ${body.channel.name} already exists.`
149 })
150 }
151 }
152
153 return next()
154 }
155 ]
156
157 const usersRemoveValidator = [
158 param('id')
159 .custom(isIdValid),
160
161 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
162 if (areValidationErrors(req, res)) return
163 if (!await checkUserIdExist(req.params.id, res)) return
164
165 const user = res.locals.user
166 if (user.username === 'root') {
167 return res.fail({ message: 'Cannot remove the root user' })
168 }
169
170 return next()
171 }
172 ]
173
174 const usersBlockingValidator = [
175 param('id')
176 .custom(isIdValid),
177 body('reason')
178 .optional()
179 .custom(isUserBlockedReasonValid),
180
181 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
182 if (areValidationErrors(req, res)) return
183 if (!await checkUserIdExist(req.params.id, res)) return
184
185 const user = res.locals.user
186 if (user.username === 'root') {
187 return res.fail({ message: 'Cannot block the root user' })
188 }
189
190 return next()
191 }
192 ]
193
194 const deleteMeValidator = [
195 (req: express.Request, res: express.Response, next: express.NextFunction) => {
196 const user = res.locals.oauth.token.User
197 if (user.username === 'root') {
198 return res.fail({ message: 'You cannot delete your root account.' })
199 }
200
201 return next()
202 }
203 ]
204
205 const usersUpdateValidator = [
206 param('id').custom(isIdValid),
207
208 body('password')
209 .optional()
210 .custom(isUserPasswordValid),
211 body('email')
212 .optional()
213 .isEmail(),
214 body('emailVerified')
215 .optional()
216 .isBoolean(),
217 body('videoQuota')
218 .optional()
219 .custom(isUserVideoQuotaValid),
220 body('videoQuotaDaily')
221 .optional()
222 .custom(isUserVideoQuotaDailyValid),
223 body('pluginAuth')
224 .optional()
225 .exists(),
226 body('role')
227 .optional()
228 .customSanitizer(toIntOrNull)
229 .custom(isUserRoleValid),
230 body('adminFlags')
231 .optional()
232 .custom(isUserAdminFlagsValid),
233
234 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
235 if (areValidationErrors(req, res, { omitBodyLog: true })) return
236 if (!await checkUserIdExist(req.params.id, res)) return
237
238 const user = res.locals.user
239 if (user.username === 'root' && req.body.role !== undefined && user.role !== req.body.role) {
240 return res.fail({ message: 'Cannot change root role.' })
241 }
242
243 return next()
244 }
245 ]
246
247 const usersUpdateMeValidator = [
248 body('displayName')
249 .optional()
250 .custom(isUserDisplayNameValid),
251 body('description')
252 .optional()
253 .custom(isUserDescriptionValid),
254 body('currentPassword')
255 .optional()
256 .custom(isUserPasswordValid),
257 body('password')
258 .optional()
259 .custom(isUserPasswordValid),
260 body('email')
261 .optional()
262 .isEmail(),
263 body('nsfwPolicy')
264 .optional()
265 .custom(isUserNSFWPolicyValid),
266 body('autoPlayVideo')
267 .optional()
268 .custom(isUserAutoPlayVideoValid),
269 body('p2pEnabled')
270 .optional()
271 .custom(isUserP2PEnabledValid).withMessage('Should have a valid p2p enabled boolean'),
272 body('videoLanguages')
273 .optional()
274 .custom(isUserVideoLanguages),
275 body('videosHistoryEnabled')
276 .optional()
277 .custom(isUserVideosHistoryEnabledValid).withMessage('Should have a valid videos history enabled boolean'),
278 body('theme')
279 .optional()
280 .custom(v => isThemeNameValid(v) && isThemeRegistered(v)),
281
282 body('noInstanceConfigWarningModal')
283 .optional()
284 .custom(v => isUserNoModal(v)).withMessage('Should have a valid noInstanceConfigWarningModal boolean'),
285 body('noWelcomeModal')
286 .optional()
287 .custom(v => isUserNoModal(v)).withMessage('Should have a valid noWelcomeModal boolean'),
288 body('noAccountSetupWarningModal')
289 .optional()
290 .custom(v => isUserNoModal(v)).withMessage('Should have a valid noAccountSetupWarningModal boolean'),
291
292 body('autoPlayNextVideo')
293 .optional()
294 .custom(v => isUserAutoPlayNextVideoValid(v)).withMessage('Should have a valid autoPlayNextVideo boolean'),
295
296 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
297 const user = res.locals.oauth.token.User
298
299 if (req.body.password || req.body.email) {
300 if (user.pluginAuth !== null) {
301 return res.fail({ message: 'You cannot update your email or password that is associated with an external auth system.' })
302 }
303
304 if (!req.body.currentPassword) {
305 return res.fail({ message: 'currentPassword parameter is missing.' })
306 }
307
308 if (await user.isPasswordMatch(req.body.currentPassword) !== true) {
309 return res.fail({
310 status: HttpStatusCode.UNAUTHORIZED_401,
311 message: 'currentPassword is invalid.'
312 })
313 }
314 }
315
316 if (areValidationErrors(req, res, { omitBodyLog: true })) return
317
318 return next()
319 }
320 ]
321
322 const usersGetValidator = [
323 param('id')
324 .custom(isIdValid),
325 query('withStats')
326 .optional()
327 .isBoolean().withMessage('Should have a valid withStats boolean'),
328
329 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
330 if (areValidationErrors(req, res)) return
331 if (!await checkUserIdExist(req.params.id, res, req.query.withStats)) return
332
333 return next()
334 }
335 ]
336
337 const usersVideoRatingValidator = [
338 isValidVideoIdParam('videoId'),
339
340 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
341 if (areValidationErrors(req, res)) return
342 if (!await doesVideoExist(req.params.videoId, res, 'id')) return
343
344 return next()
345 }
346 ]
347
348 const usersVideosValidator = [
349 query('isLive')
350 .optional()
351 .customSanitizer(toBooleanOrNull)
352 .custom(isBooleanValid).withMessage('Should have a valid isLive boolean'),
353
354 query('channelId')
355 .optional()
356 .customSanitizer(toIntOrNull)
357 .custom(isIdValid),
358
359 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
360 if (areValidationErrors(req, res)) return
361
362 if (req.query.channelId && !await doesVideoChannelIdExist(req.query.channelId, res)) return
363
364 return next()
365 }
366 ]
367
368 const ensureUserRegistrationAllowed = [
369 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
370 const allowedParams = {
371 body: req.body,
372 ip: req.ip
373 }
374
375 const allowedResult = await Hooks.wrapPromiseFun(
376 isSignupAllowed,
377 allowedParams,
378 'filter:api.user.signup.allowed.result'
379 )
380
381 if (allowedResult.allowed === false) {
382 return res.fail({
383 status: HttpStatusCode.FORBIDDEN_403,
384 message: allowedResult.errorMessage || 'User registration is not enabled or user limit is reached.'
385 })
386 }
387
388 return next()
389 }
390 ]
391
392 const ensureUserRegistrationAllowedForIP = [
393 (req: express.Request, res: express.Response, next: express.NextFunction) => {
394 const allowed = isSignupAllowedForCurrentIP(req.ip)
395
396 if (allowed === false) {
397 return res.fail({
398 status: HttpStatusCode.FORBIDDEN_403,
399 message: 'You are not on a network authorized for registration.'
400 })
401 }
402
403 return next()
404 }
405 ]
406
407 const usersAskResetPasswordValidator = [
408 body('email')
409 .isEmail(),
410
411 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
412 if (areValidationErrors(req, res)) return
413
414 const exists = await checkUserEmailExist(req.body.email, res, false)
415 if (!exists) {
416 logger.debug('User with email %s does not exist (asking reset password).', req.body.email)
417 // Do not leak our emails
418 return res.status(HttpStatusCode.NO_CONTENT_204).end()
419 }
420
421 if (res.locals.user.pluginAuth) {
422 return res.fail({
423 status: HttpStatusCode.CONFLICT_409,
424 message: 'Cannot recover password of a user that uses a plugin authentication.'
425 })
426 }
427
428 return next()
429 }
430 ]
431
432 const usersResetPasswordValidator = [
433 param('id')
434 .custom(isIdValid),
435 body('verificationString')
436 .not().isEmpty(),
437 body('password')
438 .custom(isUserPasswordValid),
439
440 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
441 if (areValidationErrors(req, res)) return
442 if (!await checkUserIdExist(req.params.id, res)) return
443
444 const user = res.locals.user
445 const redisVerificationString = await Redis.Instance.getResetPasswordVerificationString(user.id)
446
447 if (redisVerificationString !== req.body.verificationString) {
448 return res.fail({
449 status: HttpStatusCode.FORBIDDEN_403,
450 message: 'Invalid verification string.'
451 })
452 }
453
454 return next()
455 }
456 ]
457
458 const usersAskSendVerifyEmailValidator = [
459 body('email').isEmail().not().isEmpty().withMessage('Should have a valid email'),
460
461 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
462 if (areValidationErrors(req, res)) return
463
464 const exists = await checkUserEmailExist(req.body.email, res, false)
465 if (!exists) {
466 logger.debug('User with email %s does not exist (asking verify email).', req.body.email)
467 // Do not leak our emails
468 return res.status(HttpStatusCode.NO_CONTENT_204).end()
469 }
470
471 if (res.locals.user.pluginAuth) {
472 return res.fail({
473 status: HttpStatusCode.CONFLICT_409,
474 message: 'Cannot ask verification email of a user that uses a plugin authentication.'
475 })
476 }
477
478 return next()
479 }
480 ]
481
482 const usersVerifyEmailValidator = [
483 param('id')
484 .isInt().not().isEmpty().withMessage('Should have a valid id'),
485
486 body('verificationString')
487 .not().isEmpty().withMessage('Should have a valid verification string'),
488 body('isPendingEmail')
489 .optional()
490 .customSanitizer(toBooleanOrNull),
491
492 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
493 if (areValidationErrors(req, res)) return
494 if (!await checkUserIdExist(req.params.id, res)) return
495
496 const user = res.locals.user
497 const redisVerificationString = await Redis.Instance.getVerifyEmailLink(user.id)
498
499 if (redisVerificationString !== req.body.verificationString) {
500 return res.fail({
501 status: HttpStatusCode.FORBIDDEN_403,
502 message: 'Invalid verification string.'
503 })
504 }
505
506 return next()
507 }
508 ]
509
510 const usersCheckCurrentPasswordFactory = (targetUserIdGetter: (req: express.Request) => number | string) => {
511 return [
512 body('currentPassword').optional().custom(exists),
513
514 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
515 if (areValidationErrors(req, res)) return
516
517 const user = res.locals.oauth.token.User
518 const isAdminOrModerator = user.role === UserRole.ADMINISTRATOR || user.role === UserRole.MODERATOR
519 const targetUserId = forceNumber(targetUserIdGetter(req))
520
521 // Admin/moderator action on another user, skip the password check
522 if (isAdminOrModerator && targetUserId !== user.id) {
523 return next()
524 }
525
526 if (!req.body.currentPassword) {
527 return res.fail({
528 status: HttpStatusCode.BAD_REQUEST_400,
529 message: 'currentPassword is missing'
530 })
531 }
532
533 if (await user.isPasswordMatch(req.body.currentPassword) !== true) {
534 return res.fail({
535 status: HttpStatusCode.FORBIDDEN_403,
536 message: 'currentPassword is invalid.'
537 })
538 }
539
540 return next()
541 }
542 ]
543 }
544
545 const userAutocompleteValidator = [
546 param('search')
547 .isString()
548 .not().isEmpty()
549 ]
550
551 const ensureAuthUserOwnsAccountValidator = [
552 (req: express.Request, res: express.Response, next: express.NextFunction) => {
553 const user = res.locals.oauth.token.User
554
555 if (res.locals.account.id !== user.Account.id) {
556 return res.fail({
557 status: HttpStatusCode.FORBIDDEN_403,
558 message: 'Only owner of this account can access this resource.'
559 })
560 }
561
562 return next()
563 }
564 ]
565
566 const ensureCanManageChannelOrAccount = [
567 (req: express.Request, res: express.Response, next: express.NextFunction) => {
568 const user = res.locals.oauth.token.user
569 const account = res.locals.videoChannel?.Account ?? res.locals.account
570 const isUserOwner = account.userId === user.id
571
572 if (!isUserOwner && user.hasRight(UserRight.MANAGE_ANY_VIDEO_CHANNEL) === false) {
573 const message = `User ${user.username} does not have right this channel or account.`
574
575 return res.fail({
576 status: HttpStatusCode.FORBIDDEN_403,
577 message
578 })
579 }
580
581 return next()
582 }
583 ]
584
585 const ensureCanModerateUser = [
586 (req: express.Request, res: express.Response, next: express.NextFunction) => {
587 const authUser = res.locals.oauth.token.User
588 const onUser = res.locals.user
589
590 if (authUser.role === UserRole.ADMINISTRATOR) return next()
591 if (authUser.role === UserRole.MODERATOR && onUser.role === UserRole.USER) return next()
592
593 return res.fail({
594 status: HttpStatusCode.FORBIDDEN_403,
595 message: 'A moderator can only manage users.'
596 })
597 }
598 ]
599
600 // ---------------------------------------------------------------------------
601
602 export {
603 usersListValidator,
604 usersAddValidator,
605 deleteMeValidator,
606 usersRegisterValidator,
607 usersBlockingValidator,
608 usersRemoveValidator,
609 usersUpdateValidator,
610 usersUpdateMeValidator,
611 usersVideoRatingValidator,
612 usersCheckCurrentPasswordFactory,
613 ensureUserRegistrationAllowed,
614 ensureUserRegistrationAllowedForIP,
615 usersGetValidator,
616 usersVideosValidator,
617 usersAskResetPasswordValidator,
618 usersResetPasswordValidator,
619 usersAskSendVerifyEmailValidator,
620 usersVerifyEmailValidator,
621 userAutocompleteValidator,
622 ensureAuthUserOwnsAccountValidator,
623 ensureCanModerateUser,
624 ensureCanManageChannelOrAccount
625 }