Browse Source

Don't inject untrusted input

Even if it's already checked in middlewares
It's better to have safe modals too
Chocobozzz 1 year ago
parent
commit
4638cd713d
35 changed files with 101 additions and 63 deletions
  1. 0 2
      client/src/assets/player/shared/manager-options/manager-options-builder.ts
  2. 2 1
      server/controllers/api/users/my-history.ts
  3. 3 2
      server/controllers/api/users/my-video-playlists.ts
  4. 3 2
      server/controllers/api/video-playlist.ts
  5. 2 1
      server/controllers/api/videos/update.ts
  6. 2 2
      server/controllers/download.ts
  7. 3 2
      server/controllers/services.ts
  8. 2 1
      server/helpers/custom-validators/video-studio.ts
  9. 3 2
      server/helpers/video.ts
  10. 2 1
      server/lib/activitypub/collection.ts
  11. 3 2
      server/middlewares/pagination.ts
  12. 2 1
      server/middlewares/validators/abuse.ts
  13. 2 1
      server/middlewares/validators/redundancy.ts
  14. 2 1
      server/middlewares/validators/shared/abuses.ts
  15. 3 2
      server/middlewares/validators/shared/accounts.ts
  16. 2 1
      server/middlewares/validators/shared/users.ts
  17. 4 3
      server/middlewares/validators/shared/video-comments.ts
  18. 2 1
      server/middlewares/validators/shared/video-ownerships.ts
  19. 2 1
      server/middlewares/validators/users.ts
  20. 2 1
      server/middlewares/validators/videos/video-imports.ts
  21. 2 1
      server/middlewares/validators/videos/video-playlists.ts
  22. 3 2
      server/models/abuse/abuse-query-builder.ts
  23. 2 2
      server/models/actor/actor.ts
  24. 2 1
      server/models/user/user-notification.ts
  25. 8 7
      server/models/user/user.ts
  26. 3 2
      server/models/utils.ts
  27. 3 2
      server/models/video/sql/video/videos-id-list-query-builder.ts
  28. 2 2
      server/models/video/video-channel.ts
  29. 13 8
      server/models/video/video-playlist-element.ts
  30. 3 2
      server/models/video/video-share.ts
  31. 2 2
      server/tools/peertube-redundancy.ts
  32. 1 0
      shared/core-utils/common/index.ts
  33. 7 0
      shared/core-utils/common/number.ts
  34. 2 1
      shared/extra-utils/ffprobe.ts
  35. 2 1
      shared/server-commands/miscs/sql-command.ts

+ 0 - 2
client/src/assets/player/shared/manager-options/manager-options-builder.ts

@@ -105,8 +105,6 @@ export class ManagerOptionsBuilder {
       Object.assign(videojsOptions, { language: commonOptions.language })
     }
 
-    console.log(videojsOptions)
-
     return videojsOptions
   }
 

+ 2 - 1
server/controllers/api/users/my-history.ts

@@ -1,3 +1,4 @@
+import { forceNumber } from '@shared/core-utils'
 import express from 'express'
 import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes'
 import { getFormattedObjects } from '../../../helpers/utils'
@@ -55,7 +56,7 @@ async function listMyVideosHistory (req: express.Request, res: express.Response)
 async function removeUserHistoryElement (req: express.Request, res: express.Response) {
   const user = res.locals.oauth.token.User
 
-  await UserVideoHistoryModel.removeUserHistoryElement(user, parseInt(req.params.videoId + ''))
+  await UserVideoHistoryModel.removeUserHistoryElement(user, forceNumber(req.params.videoId))
 
   return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
 }

+ 3 - 2
server/controllers/api/users/my-video-playlists.ts

@@ -1,5 +1,6 @@
-import { uuidToShort } from '@shared/extra-utils'
 import express from 'express'
+import { forceNumber } from '@shared/core-utils'
+import { uuidToShort } from '@shared/extra-utils'
 import { VideosExistInPlaylists } from '../../../../shared/models/videos/playlist/video-exist-in-playlist.model'
 import { asyncMiddleware, authenticate } from '../../../middlewares'
 import { doVideosInPlaylistExistValidator } from '../../../middlewares/validators/videos/video-playlists'
@@ -22,7 +23,7 @@ export {
 // ---------------------------------------------------------------------------
 
 async function doVideosInPlaylistExist (req: express.Request, res: express.Response) {
-  const videoIds = req.query.videoIds.map(i => parseInt(i + '', 10))
+  const videoIds = req.query.videoIds.map(i => forceNumber(i))
   const user = res.locals.oauth.token.User
 
   const results = await VideoPlaylistModel.listPlaylistSummariesOf(user.Account.id, videoIds)

+ 3 - 2
server/controllers/api/video-playlist.ts

@@ -46,6 +46,7 @@ import {
 import { AccountModel } from '../../models/account/account'
 import { VideoPlaylistModel } from '../../models/video/video-playlist'
 import { VideoPlaylistElementModel } from '../../models/video/video-playlist-element'
+import { forceNumber } from '@shared/core-utils'
 
 const reqThumbnailFile = createReqFiles([ 'thumbnailfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT)
 
@@ -245,7 +246,7 @@ async function updateVideoPlaylist (req: express.Request, res: express.Response)
       if (videoPlaylistInfoToUpdate.description !== undefined) videoPlaylistInstance.description = videoPlaylistInfoToUpdate.description
 
       if (videoPlaylistInfoToUpdate.privacy !== undefined) {
-        videoPlaylistInstance.privacy = parseInt(videoPlaylistInfoToUpdate.privacy.toString(), 10)
+        videoPlaylistInstance.privacy = forceNumber(videoPlaylistInfoToUpdate.privacy)
 
         if (wasNotPrivatePlaylist === true && videoPlaylistInstance.privacy === VideoPlaylistPrivacy.PRIVATE) {
           await sendDeleteVideoPlaylist(videoPlaylistInstance, t)
@@ -424,7 +425,7 @@ async function reorderVideosPlaylist (req: express.Request, res: express.Respons
 
     const endOldPosition = oldPosition + reorderLength - 1
     // Insert our reordered elements in their place (update)
-    await VideoPlaylistElementModel.reassignPositionOf(videoPlaylist.id, oldPosition, endOldPosition, newPosition, t)
+    await VideoPlaylistElementModel.reassignPositionOf({ videoPlaylistId: videoPlaylist.id, firstPosition: oldPosition, endPosition: endOldPosition, newPosition, transaction: t })
 
     // Decrease positions of elements after the old position of our ordered elements (decrease)
     await VideoPlaylistElementModel.increasePositionOf(videoPlaylist.id, oldPosition, -reorderLength, t)

+ 2 - 1
server/controllers/api/videos/update.ts

@@ -19,6 +19,7 @@ import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videosU
 import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update'
 import { VideoModel } from '../../../models/video/video'
 import { VideoPathManager } from '@server/lib/video-path-manager'
+import { forceNumber } from '@shared/core-utils'
 
 const lTags = loggerTagsFactory('api', 'video')
 const auditLogger = auditLoggerFactory('videos')
@@ -174,7 +175,7 @@ async function updateVideoPrivacy (options: {
   const { videoInstance, videoInfoToUpdate, hadPrivacyForFederation, transaction } = options
   const isNewVideo = videoInstance.isNewVideo(videoInfoToUpdate.privacy)
 
-  const newPrivacy = parseInt(videoInfoToUpdate.privacy.toString(), 10)
+  const newPrivacy = forceNumber(videoInfoToUpdate.privacy)
   setVideoPrivacy(videoInstance, newPrivacy)
 
   // Unfederate the video if the new privacy is not compatible with federation

+ 2 - 2
server/controllers/download.ts

@@ -5,7 +5,7 @@ import { VideosTorrentCache } from '@server/lib/files-cache/videos-torrent-cache
 import { Hooks } from '@server/lib/plugins/hooks'
 import { VideoPathManager } from '@server/lib/video-path-manager'
 import { MStreamingPlaylist, MVideo, MVideoFile, MVideoFullLight } from '@server/types/models'
-import { addQueryParams } from '@shared/core-utils'
+import { addQueryParams, forceNumber } from '@shared/core-utils'
 import { HttpStatusCode, VideoStorage, VideoStreamingPlaylistType } from '@shared/models'
 import { STATIC_DOWNLOAD_PATHS } from '../initializers/constants'
 import { asyncMiddleware, optionalAuthenticate, videosDownloadValidator } from '../middlewares'
@@ -132,7 +132,7 @@ async function downloadHLSVideoFile (req: express.Request, res: express.Response
 }
 
 function getVideoFile (req: express.Request, files: MVideoFile[]) {
-  const resolution = parseInt(req.params.resolution, 10)
+  const resolution = forceNumber(req.params.resolution)
   return files.find(f => f.resolution === resolution)
 }
 

+ 3 - 2
server/controllers/services.ts

@@ -4,6 +4,7 @@ import { escapeHTML } from '@shared/core-utils/renderer'
 import { EMBED_SIZE, PREVIEWS_SIZE, THUMBNAILS_SIZE, WEBSERVER } from '../initializers/constants'
 import { asyncMiddleware, oembedValidator } from '../middlewares'
 import { accountNameWithHostGetValidator } from '../middlewares/validators'
+import { forceNumber } from '@shared/core-utils'
 
 const servicesRouter = express.Router()
 
@@ -108,8 +109,8 @@ function buildOEmbed (options: {
   const { req, previewSize, previewPath, title, channel, embedPath } = options
 
   const webserverUrl = WEBSERVER.URL
-  const maxHeight = parseInt(req.query.maxheight, 10)
-  const maxWidth = parseInt(req.query.maxwidth, 10)
+  const maxHeight = forceNumber(req.query.maxheight)
+  const maxWidth = forceNumber(req.query.maxwidth)
 
   const embedUrl = webserverUrl + embedPath
   const embedTitle = escapeHTML(title)

+ 2 - 1
server/helpers/custom-validators/video-studio.ts

@@ -4,6 +4,7 @@ import { buildTaskFileFieldname } from '@server/lib/video-studio'
 import { VideoStudioTask } from '@shared/models'
 import { isArray } from './misc'
 import { isVideoFileMimeTypeValid, isVideoImageValid } from './videos'
+import { forceNumber } from '@shared/core-utils'
 
 function isValidStudioTasksArray (tasks: any) {
   if (!isArray(tasks)) return false
@@ -24,7 +25,7 @@ function isStudioCutTaskValid (task: VideoStudioTask) {
 
   if (!start || !end) return true
 
-  return parseInt(start + '') < parseInt(end + '')
+  return forceNumber(start) < forceNumber(end)
 }
 
 function isStudioTaskAddIntroOutroValid (task: VideoStudioTask, indice: number, files: Express.Multer.File[]) {

+ 3 - 2
server/helpers/video.ts

@@ -2,6 +2,7 @@ import { Response } from 'express'
 import { CONFIG } from '@server/initializers/config'
 import { isStreamingPlaylist, MStreamingPlaylistVideo, MVideo } from '@server/types/models'
 import { VideoPrivacy, VideoState } from '@shared/models'
+import { forceNumber } from '@shared/core-utils'
 
 function getVideoWithAttributes (res: Response) {
   return res.locals.videoAPI || res.locals.videoAll || res.locals.onlyVideo
@@ -14,14 +15,14 @@ function extractVideo (videoOrPlaylist: MVideo | MStreamingPlaylistVideo) {
 }
 
 function isPrivacyForFederation (privacy: VideoPrivacy) {
-  const castedPrivacy = parseInt(privacy + '', 10)
+  const castedPrivacy = forceNumber(privacy)
 
   return castedPrivacy === VideoPrivacy.PUBLIC ||
     (CONFIG.FEDERATION.VIDEOS.FEDERATE_UNLISTED === true && castedPrivacy === VideoPrivacy.UNLISTED)
 }
 
 function isStateForFederation (state: VideoState) {
-  const castedState = parseInt(state + '', 10)
+  const castedState = forceNumber(state)
 
   return castedState === VideoState.PUBLISHED || castedState === VideoState.WAITING_FOR_LIVE || castedState === VideoState.LIVE_ENDED
 }

+ 2 - 1
server/lib/activitypub/collection.ts

@@ -3,6 +3,7 @@ import validator from 'validator'
 import { pageToStartAndCount } from '@server/helpers/core-utils'
 import { ACTIVITY_PUB } from '@server/initializers/constants'
 import { ResultList } from '@shared/models'
+import { forceNumber } from '@shared/core-utils'
 
 type ActivityPubCollectionPaginationHandler = (start: number, count: number) => Bluebird<ResultList<any>> | Promise<ResultList<any>>
 
@@ -33,7 +34,7 @@ async function activityPubCollectionPagination (
   let prev: string | undefined
 
   // Assert page is a number
-  page = parseInt(page, 10)
+  page = forceNumber(page)
 
   // There are more results
   if (result.total > page * size) {

+ 3 - 2
server/middlewares/pagination.ts

@@ -1,12 +1,13 @@
 import express from 'express'
+import { forceNumber } from '@shared/core-utils'
 import { PAGINATION } from '../initializers/constants'
 
 function setDefaultPagination (req: express.Request, res: express.Response, next: express.NextFunction) {
   if (!req.query.start) req.query.start = 0
-  else req.query.start = parseInt(req.query.start, 10)
+  else req.query.start = forceNumber(req.query.start)
 
   if (!req.query.count) req.query.count = PAGINATION.GLOBAL.COUNT.DEFAULT
-  else req.query.count = parseInt(req.query.count, 10)
+  else req.query.count = forceNumber(req.query.count)
 
   return next()
 }

+ 2 - 1
server/middlewares/validators/abuse.ts

@@ -18,6 +18,7 @@ import { AbuseMessageModel } from '@server/models/abuse/abuse-message'
 import { AbuseCreate, UserRight } from '@shared/models'
 import { HttpStatusCode } from '../../../shared/models/http/http-error-codes'
 import { areValidationErrors, doesAbuseExist, doesAccountIdExist, doesCommentIdExist, doesVideoExist } from './shared'
+import { forceNumber } from '@shared/core-utils'
 
 const abuseReportValidator = [
   body('account.id')
@@ -216,7 +217,7 @@ const deleteAbuseMessageValidator = [
     const user = res.locals.oauth.token.user
     const abuse = res.locals.abuse
 
-    const messageId = parseInt(req.params.messageId + '', 10)
+    const messageId = forceNumber(req.params.messageId)
     const abuseMessage = await AbuseMessageModel.loadByIdAndAbuseId(messageId, abuse.id)
 
     if (!abuseMessage) {

+ 2 - 1
server/middlewares/validators/redundancy.ts

@@ -1,6 +1,7 @@
 import express from 'express'
 import { body, param, query } from 'express-validator'
 import { isVideoRedundancyTarget } from '@server/helpers/custom-validators/video-redundancies'
+import { forceNumber } from '@shared/core-utils'
 import { HttpStatusCode } from '../../../shared/models/http/http-error-codes'
 import {
   exists,
@@ -171,7 +172,7 @@ const removeVideoRedundancyValidator = [
   async (req: express.Request, res: express.Response, next: express.NextFunction) => {
     if (areValidationErrors(req, res)) return
 
-    const redundancy = await VideoRedundancyModel.loadByIdWithVideo(parseInt(req.params.redundancyId, 10))
+    const redundancy = await VideoRedundancyModel.loadByIdWithVideo(forceNumber(req.params.redundancyId))
     if (!redundancy) {
       return res.fail({
         status: HttpStatusCode.NOT_FOUND_404,

+ 2 - 1
server/middlewares/validators/shared/abuses.ts

@@ -1,9 +1,10 @@
 import { Response } from 'express'
 import { AbuseModel } from '@server/models/abuse/abuse'
 import { HttpStatusCode } from '@shared/models'
+import { forceNumber } from '@shared/core-utils'
 
 async function doesAbuseExist (abuseId: number | string, res: Response) {
-  const abuse = await AbuseModel.loadByIdWithReporter(parseInt(abuseId + '', 10))
+  const abuse = await AbuseModel.loadByIdWithReporter(forceNumber(abuseId))
 
   if (!abuse) {
     res.fail({

+ 3 - 2
server/middlewares/validators/shared/accounts.ts

@@ -2,10 +2,11 @@ import { Response } from 'express'
 import { AccountModel } from '@server/models/account/account'
 import { UserModel } from '@server/models/user/user'
 import { MAccountDefault } from '@server/types/models'
+import { forceNumber } from '@shared/core-utils'
 import { HttpStatusCode } from '@shared/models'
 
 function doesAccountIdExist (id: number | string, res: Response, sendNotFound = true) {
-  const promise = AccountModel.load(parseInt(id + '', 10))
+  const promise = AccountModel.load(forceNumber(id))
 
   return doesAccountExist(promise, res, sendNotFound)
 }
@@ -40,7 +41,7 @@ async function doesAccountExist (p: Promise<MAccountDefault>, res: Response, sen
 }
 
 async function doesUserFeedTokenCorrespond (id: number, token: string, res: Response) {
-  const user = await UserModel.loadByIdWithChannels(parseInt(id + '', 10))
+  const user = await UserModel.loadByIdWithChannels(forceNumber(id))
 
   if (token !== user.feedToken) {
     res.fail({

+ 2 - 1
server/middlewares/validators/shared/users.ts

@@ -2,10 +2,11 @@ import express from 'express'
 import { ActorModel } from '@server/models/actor/actor'
 import { UserModel } from '@server/models/user/user'
 import { MUserDefault } from '@server/types/models'
+import { forceNumber } from '@shared/core-utils'
 import { HttpStatusCode } from '@shared/models'
 
 function checkUserIdExist (idArg: number | string, res: express.Response, withStats = false) {
-  const id = parseInt(idArg + '', 10)
+  const id = forceNumber(idArg)
   return checkUserExist(() => UserModel.loadByIdWithChannels(id, withStats), res)
 }
 

+ 4 - 3
server/middlewares/validators/shared/video-comments.ts

@@ -1,10 +1,11 @@
 import express from 'express'
 import { VideoCommentModel } from '@server/models/video/video-comment'
 import { MVideoId } from '@server/types/models'
+import { forceNumber } from '@shared/core-utils'
 import { HttpStatusCode, ServerErrorCode } from '@shared/models'
 
 async function doesVideoCommentThreadExist (idArg: number | string, video: MVideoId, res: express.Response) {
-  const id = parseInt(idArg + '', 10)
+  const id = forceNumber(idArg)
   const videoComment = await VideoCommentModel.loadById(id)
 
   if (!videoComment) {
@@ -33,7 +34,7 @@ async function doesVideoCommentThreadExist (idArg: number | string, video: MVide
 }
 
 async function doesVideoCommentExist (idArg: number | string, video: MVideoId, res: express.Response) {
-  const id = parseInt(idArg + '', 10)
+  const id = forceNumber(idArg)
   const videoComment = await VideoCommentModel.loadByIdAndPopulateVideoAndAccountAndReply(id)
 
   if (!videoComment) {
@@ -57,7 +58,7 @@ async function doesVideoCommentExist (idArg: number | string, video: MVideoId, r
 }
 
 async function doesCommentIdExist (idArg: number | string, res: express.Response) {
-  const id = parseInt(idArg + '', 10)
+  const id = forceNumber(idArg)
   const videoComment = await VideoCommentModel.loadByIdAndPopulateVideoAndAccountAndReply(id)
 
   if (!videoComment) {

+ 2 - 1
server/middlewares/validators/shared/video-ownerships.ts

@@ -1,9 +1,10 @@
 import express from 'express'
 import { VideoChangeOwnershipModel } from '@server/models/video/video-change-ownership'
+import { forceNumber } from '@shared/core-utils'
 import { HttpStatusCode } from '@shared/models'
 
 async function doesChangeVideoOwnershipExist (idArg: number | string, res: express.Response) {
-  const id = parseInt(idArg + '', 10)
+  const id = forceNumber(idArg)
   const videoChangeOwnership = await VideoChangeOwnershipModel.load(id)
 
   if (!videoChangeOwnership) {

+ 2 - 1
server/middlewares/validators/users.ts

@@ -1,6 +1,7 @@
 import express from 'express'
 import { body, param, query } from 'express-validator'
 import { Hooks } from '@server/lib/plugins/hooks'
+import { forceNumber } from '@shared/core-utils'
 import { HttpStatusCode, UserRegister, UserRight, UserRole } from '@shared/models'
 import { exists, isBooleanValid, isIdValid, toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc'
 import { isThemeNameValid } from '../../helpers/custom-validators/plugins'
@@ -515,7 +516,7 @@ const usersCheckCurrentPasswordFactory = (targetUserIdGetter: (req: express.Requ
 
       const user = res.locals.oauth.token.User
       const isAdminOrModerator = user.role === UserRole.ADMINISTRATOR || user.role === UserRole.MODERATOR
-      const targetUserId = parseInt(targetUserIdGetter(req) + '')
+      const targetUserId = forceNumber(targetUserIdGetter(req))
 
       // Admin/moderator action on another user, skip the password check
       if (isAdminOrModerator && targetUserId !== user.id) {

+ 2 - 1
server/middlewares/validators/videos/video-imports.ts

@@ -4,6 +4,7 @@ import { isResolvingToUnicastOnly } from '@server/helpers/dns'
 import { isPreImportVideoAccepted } from '@server/lib/moderation'
 import { Hooks } from '@server/lib/plugins/hooks'
 import { MUserAccountId, MVideoImport } from '@server/types/models'
+import { forceNumber } from '@shared/core-utils'
 import { HttpStatusCode, UserRight, VideoImportState } from '@shared/models'
 import { VideoImportCreate } from '@shared/models/videos/import/video-import-create.model'
 import { isIdValid, toIntOrNull } from '../../../helpers/custom-validators/misc'
@@ -130,7 +131,7 @@ const videoImportCancelValidator = [
   async (req: express.Request, res: express.Response, next: express.NextFunction) => {
     if (areValidationErrors(req, res)) return
 
-    if (!await doesVideoImportExist(parseInt(req.params.id), res)) return
+    if (!await doesVideoImportExist(forceNumber(req.params.id), res)) return
     if (!checkUserCanManageImport(res.locals.oauth.token.user, res.locals.videoImport, res)) return
 
     if (res.locals.videoImport.state !== VideoImportState.PENDING) {

+ 2 - 1
server/middlewares/validators/videos/video-playlists.ts

@@ -2,6 +2,7 @@ import express from 'express'
 import { body, param, query, ValidationChain } from 'express-validator'
 import { ExpressPromiseHandler } from '@server/types/express-handler'
 import { MUserAccountId } from '@server/types/models'
+import { forceNumber } from '@shared/core-utils'
 import {
   HttpStatusCode,
   UserRight,
@@ -258,7 +259,7 @@ const videoPlaylistElementAPGetValidator = [
   async (req: express.Request, res: express.Response, next: express.NextFunction) => {
     if (areValidationErrors(req, res)) return
 
-    const playlistElementId = parseInt(req.params.playlistElementId + '', 10)
+    const playlistElementId = forceNumber(req.params.playlistElementId)
     const playlistId = req.params.playlistId
 
     const videoPlaylistElement = await VideoPlaylistElementModel.loadByPlaylistAndElementIdForAP(playlistId, playlistElementId)

+ 3 - 2
server/models/abuse/abuse-query-builder.ts

@@ -1,5 +1,6 @@
 
 import { exists } from '@server/helpers/custom-validators/misc'
+import { forceNumber } from '@shared/core-utils'
 import { AbuseFilter, AbuseState, AbuseVideoIs } from '@shared/models'
 import { buildBlockedAccountSQL, buildDirectionAndField } from '../utils'
 
@@ -135,12 +136,12 @@ function buildAbuseListQuery (options: BuildAbusesQueryOptions, type: 'count' |
     }
 
     if (exists(options.count)) {
-      const count = parseInt(options.count + '', 10)
+      const count = forceNumber(options.count)
       suffix += `LIMIT ${count} `
     }
 
     if (exists(options.start)) {
-      const start = parseInt(options.start + '', 10)
+      const start = forceNumber(options.start)
       suffix += `OFFSET ${start} `
     }
   }

+ 2 - 2
server/models/actor/actor.ts

@@ -18,7 +18,7 @@ import {
 import { activityPubContextify } from '@server/lib/activitypub/context'
 import { getBiggestActorImage } from '@server/lib/actor-image'
 import { ModelCache } from '@server/models/model-cache'
-import { getLowercaseExtension } from '@shared/core-utils'
+import { forceNumber, getLowercaseExtension } from '@shared/core-utils'
 import { ActivityIconObject, ActivityPubActorType, ActorImageType } from '@shared/models'
 import { AttributesOnly } from '@shared/typescript-utils'
 import {
@@ -446,7 +446,7 @@ export class ActorModel extends Model<Partial<AttributesOnly<ActorModel>>> {
   }
 
   static rebuildFollowsCount (ofId: number, type: 'followers' | 'following', transaction?: Transaction) {
-    const sanitizedOfId = parseInt(ofId + '', 10)
+    const sanitizedOfId = forceNumber(ofId)
     const where = { id: sanitizedOfId }
 
     let columnToUpdate: string

+ 2 - 1
server/models/user/user-notification.ts

@@ -2,6 +2,7 @@ import { ModelIndexesOptions, Op, WhereOptions } from 'sequelize'
 import { AllowNull, BelongsTo, Column, CreatedAt, Default, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
 import { getBiggestActorImage } from '@server/lib/actor-image'
 import { UserNotificationIncludes, UserNotificationModelForApi } from '@server/types/models/user'
+import { forceNumber } from '@shared/core-utils'
 import { uuidToShort } from '@shared/extra-utils'
 import { UserNotification, UserNotificationType } from '@shared/models'
 import { AttributesOnly } from '@shared/typescript-utils'
@@ -284,7 +285,7 @@ export class UserNotificationModel extends Model<Partial<AttributesOnly<UserNoti
   }
 
   static removeNotificationsOf (options: { id: number, type: 'account' | 'server', forUserId?: number }) {
-    const id = parseInt(options.id + '', 10)
+    const id = forceNumber(options.id)
 
     function buildAccountWhereQuery (base: string) {
       const whereSuffix = options.forUserId

+ 8 - 7
server/models/user/user.ts

@@ -70,6 +70,7 @@ import { VideoImportModel } from '../video/video-import'
 import { VideoLiveModel } from '../video/video-live'
 import { VideoPlaylistModel } from '../video/video-playlist'
 import { UserNotificationSettingModel } from './user-notification-setting'
+import { forceNumber } from '@shared/core-utils'
 
 enum ScopeNames {
   FOR_ME_API = 'FOR_ME_API',
@@ -900,27 +901,27 @@ export class UserModel extends Model<Partial<AttributesOnly<UserModel>>> {
       videoQuotaDaily: this.videoQuotaDaily,
 
       videoQuotaUsed: videoQuotaUsed !== undefined
-        ? parseInt(videoQuotaUsed + '', 10) + LiveQuotaStore.Instance.getLiveQuotaOf(this.id)
+        ? forceNumber(videoQuotaUsed) + LiveQuotaStore.Instance.getLiveQuotaOf(this.id)
         : undefined,
 
       videoQuotaUsedDaily: videoQuotaUsedDaily !== undefined
-        ? parseInt(videoQuotaUsedDaily + '', 10) + LiveQuotaStore.Instance.getLiveQuotaOf(this.id)
+        ? forceNumber(videoQuotaUsedDaily) + LiveQuotaStore.Instance.getLiveQuotaOf(this.id)
         : undefined,
 
       videosCount: videosCount !== undefined
-        ? parseInt(videosCount + '', 10)
+        ? forceNumber(videosCount)
         : undefined,
       abusesCount: abusesCount
-        ? parseInt(abusesCount, 10)
+        ? forceNumber(abusesCount)
         : undefined,
       abusesAcceptedCount: abusesAcceptedCount
-        ? parseInt(abusesAcceptedCount, 10)
+        ? forceNumber(abusesAcceptedCount)
         : undefined,
       abusesCreatedCount: abusesCreatedCount !== undefined
-        ? parseInt(abusesCreatedCount + '', 10)
+        ? forceNumber(abusesCreatedCount)
         : undefined,
       videoCommentsCount: videoCommentsCount !== undefined
-        ? parseInt(videoCommentsCount + '', 10)
+        ? forceNumber(videoCommentsCount)
         : undefined,
 
       noInstanceConfigWarningModal: this.noInstanceConfigWarningModal,

+ 3 - 2
server/models/utils.ts

@@ -1,5 +1,6 @@
 import { literal, Op, OrderItem, Sequelize } from 'sequelize'
 import validator from 'validator'
+import { forceNumber } from '@shared/core-utils'
 
 type SortType = { sortModel: string, sortValue: string }
 
@@ -202,7 +203,7 @@ function buildBlockedAccountSQLOptimized (columnNameJoin: string, blockerIds: nu
 }
 
 function buildServerIdsFollowedBy (actorId: any) {
-  const actorIdNumber = parseInt(actorId + '', 10)
+  const actorIdNumber = forceNumber(actorId)
 
   return '(' +
     'SELECT "actor"."serverId" FROM "actorFollow" ' +
@@ -218,7 +219,7 @@ function buildWhereIdOrUUID (id: number | string) {
 function parseAggregateResult (result: any) {
   if (!result) return 0
 
-  const total = parseInt(result + '', 10)
+  const total = forceNumber(result)
   if (isNaN(total)) return 0
 
   return total

+ 3 - 2
server/models/video/sql/video/videos-id-list-query-builder.ts

@@ -6,6 +6,7 @@ import { buildDirectionAndField, createSafeIn, parseRowCountResult } from '@serv
 import { MUserAccountId, MUserId } from '@server/types/models'
 import { VideoInclude, VideoPrivacy, VideoState } from '@shared/models'
 import { AbstractRunQuery } from '../../../shared/abstract-run-query'
+import { forceNumber } from '@shared/core-utils'
 
 /**
  *
@@ -689,12 +690,12 @@ export class VideosIdListQueryBuilder extends AbstractRunQuery {
   }
 
   private setLimit (countArg: number) {
-    const count = parseInt(countArg + '', 10)
+    const count = forceNumber(countArg)
     this.limit = `LIMIT ${count}`
   }
 
   private setOffset (startArg: number) {
-    const start = parseInt(startArg + '', 10)
+    const start = forceNumber(startArg)
     this.offset = `OFFSET ${start}`
   }
 }

+ 2 - 2
server/models/video/video-channel.ts

@@ -19,7 +19,7 @@ import {
 } from 'sequelize-typescript'
 import { CONFIG } from '@server/initializers/config'
 import { MAccountActor } from '@server/types/models'
-import { pick } from '@shared/core-utils'
+import { forceNumber, pick } from '@shared/core-utils'
 import { AttributesOnly } from '@shared/typescript-utils'
 import { ActivityPubActor } from '../../../shared/models/activitypub'
 import { VideoChannel, VideoChannelSummary } from '../../../shared/models/videos'
@@ -280,7 +280,7 @@ export type SummaryOptions = {
     ]
   },
   [ScopeNames.WITH_STATS]: (options: AvailableWithStatsOptions = { daysPrior: 30 }) => {
-    const daysPrior = parseInt(options.daysPrior + '', 10)
+    const daysPrior = forceNumber(options.daysPrior)
 
     return {
       attributes: {

+ 13 - 8
server/models/video/video-playlist-element.ts

@@ -23,6 +23,7 @@ import {
   MVideoPlaylistElementVideoUrlPlaylistPrivacy,
   MVideoPlaylistVideoThumbnail
 } from '@server/types/models/video/video-playlist-element'
+import { forceNumber } from '@shared/core-utils'
 import { AttributesOnly } from '@shared/typescript-utils'
 import { PlaylistElementObject } from '../../../shared/models/activitypub/objects/playlist-element-object'
 import { VideoPrivacy } from '../../../shared/models/videos'
@@ -185,7 +186,9 @@ export class VideoPlaylistElementModel extends Model<Partial<AttributesOnly<Vide
     playlistId: number | string,
     playlistElementId: number
   ): Promise<MVideoPlaylistElementVideoUrlPlaylistPrivacy> {
-    const playlistWhere = validator.isUUID('' + playlistId) ? { uuid: playlistId } : { id: playlistId }
+    const playlistWhere = validator.isUUID('' + playlistId)
+      ? { uuid: playlistId }
+      : { id: playlistId }
 
     const query = {
       include: [
@@ -262,13 +265,15 @@ export class VideoPlaylistElementModel extends Model<Partial<AttributesOnly<Vide
       .then(position => position ? position + 1 : 1)
   }
 
-  static reassignPositionOf (
-    videoPlaylistId: number,
-    firstPosition: number,
-    endPosition: number,
-    newPosition: number,
+  static reassignPositionOf (options: {
+    videoPlaylistId: number
+    firstPosition: number
+    endPosition: number
+    newPosition: number
     transaction?: Transaction
-  ) {
+  }) {
+    const { videoPlaylistId, firstPosition, endPosition, newPosition, transaction } = options
+
     const query = {
       where: {
         videoPlaylistId,
@@ -281,7 +286,7 @@ export class VideoPlaylistElementModel extends Model<Partial<AttributesOnly<Vide
       validate: false // We use a literal to update the position
     }
 
-    const positionQuery = Sequelize.literal(`${newPosition} + "position" - ${firstPosition}`)
+    const positionQuery = Sequelize.literal(`${forceNumber(newPosition)} + "position" - ${forceNumber(firstPosition)}`)
     return VideoPlaylistElementModel.update({ position: positionQuery }, query)
   }
 

+ 3 - 2
server/models/video/video-share.ts

@@ -1,5 +1,6 @@
 import { literal, Op, QueryTypes, Transaction } from 'sequelize'
 import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
+import { forceNumber } from '@shared/core-utils'
 import { AttributesOnly } from '@shared/typescript-utils'
 import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
 import { CONSTRAINTS_FIELDS } from '../../initializers/constants'
@@ -123,7 +124,7 @@ export class VideoShareModel extends Model<Partial<AttributesOnly<VideoShareMode
   }
 
   static loadActorsWhoSharedVideosOf (actorOwnerId: number, t: Transaction): Promise<MActorDefault[]> {
-    const safeOwnerId = parseInt(actorOwnerId + '', 10)
+    const safeOwnerId = forceNumber(actorOwnerId)
 
     // /!\ On actor model
     const query = {
@@ -148,7 +149,7 @@ export class VideoShareModel extends Model<Partial<AttributesOnly<VideoShareMode
   }
 
   static loadActorsByVideoChannel (videoChannelId: number, t: Transaction): Promise<MActorDefault[]> {
-    const safeChannelId = parseInt(videoChannelId + '', 10)
+    const safeChannelId = forceNumber(videoChannelId)
 
     // /!\ On actor model
     const query = {

+ 2 - 2
server/tools/peertube-redundancy.ts

@@ -2,7 +2,7 @@ import CliTable3 from 'cli-table3'
 import { Command, program } from 'commander'
 import { URL } from 'url'
 import validator from 'validator'
-import { uniqify } from '@shared/core-utils'
+import { forceNumber, uniqify } from '@shared/core-utils'
 import { HttpStatusCode, VideoRedundanciesTarget } from '@shared/models'
 import { assignToken, buildServer, getServerCredentials } from './cli'
 
@@ -138,7 +138,7 @@ async function removeRedundancyCLI (options: { video: number }, command: Command
     process.exit(-1)
   }
 
-  const videoId = parseInt(options.video + '', 10)
+  const videoId = forceNumber(options.video)
 
   const myVideoRedundancies = await server.redundancy.listVideos({ target: 'my-videos' })
   let videoRedundancy = myVideoRedundancies.data.find(r => videoId === r.id)

+ 1 - 0
shared/core-utils/common/index.ts

@@ -2,6 +2,7 @@ export * from './array'
 export * from './random'
 export * from './date'
 export * from './env'
+export * from './number'
 export * from './object'
 export * from './path'
 export * from './regexp'

+ 7 - 0
shared/core-utils/common/number.ts

@@ -0,0 +1,7 @@
+function forceNumber (value: any) {
+  return parseInt(value + '')
+}
+
+export {
+  forceNumber
+}

+ 2 - 1
shared/extra-utils/ffprobe.ts

@@ -1,4 +1,5 @@
 import { ffprobe, FfprobeData } from 'fluent-ffmpeg'
+import { forceNumber } from '@shared/core-utils'
 import { VideoFileMetadata, VideoResolution } from '@shared/models/videos'
 
 /**
@@ -55,7 +56,7 @@ async function getAudioStream (videoPath: string, existingProbe?: FfprobeData) {
       return {
         absolutePath: data.format.filename,
         audioStream,
-        bitrate: parseInt(audioStream['bit_rate'] + '', 10)
+        bitrate: forceNumber(audioStream['bit_rate'])
       }
     }
   }

+ 2 - 1
shared/server-commands/miscs/sql-command.ts

@@ -1,4 +1,5 @@
 import { QueryTypes, Sequelize } from 'sequelize'
+import { forceNumber } from '@shared/core-utils'
 import { AbstractCommand } from '../shared'
 
 export class SQLCommand extends AbstractCommand {
@@ -63,7 +64,7 @@ export class SQLCommand extends AbstractCommand {
 
     if (!total) return 0
 
-    return parseInt(total + '', 10)
+    return forceNumber(total)
   }
 
   getActorImage (filename: string) {