CVE-2022-0133 (published 2022-01-10T14:12Z)

peertube is vulnerable to Improper Access Control

Severity: MEDIUM
CVSS v2 base score: 5.0 (AV:N/AC:L/Au:N/C:P/I:N/A:N)
CVSS v3 base score: 7.5, HIGH (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N)

Weakness information:
cwe_id: CWE-284
cwe_name: Improper Access Control
description: The product does not restrict or incorrectly restricts access to a resource from an unauthorized actor.
extended_description: 
            Access control involves the use of several protection mechanisms such as:
            
               Authentication (proving the identity of an actor)
               Authorization (ensuring that a given actor can access a resource), and
               Accountability (tracking of activities that were performed)
            
            When any mechanism is not applied or otherwise fails, attackers can compromise the security of the product by gaining privileges, reading sensitive information, executing commands, evading detection, etc.
            There are two distinct behaviors that can introduce access control weaknesses:
               
                  Specification: incorrect privileges, permissions, ownership, etc. are explicitly specified for either the user or the resource (for example, setting a password file to be world-writable, or giving administrator capabilities to a guest user). This action could be performed by the program or the administrator.
                  Enforcement: the mechanism contains errors that prevent it from properly enforcing the specified access control requirements (e.g., allowing the user to specify their own privileges, or allowing a syntactically-incorrect ACL to produce insecure settings). This problem occurs within the program itself, in that it does not actually enforce the intended security policy that the administrator specifies.
               
         
		 

Fix information:
repo_url: https://github.com/chocobozzz/peertube
commit_details: repo_url: https://github.com/chocobozzz/peertube
author: Chocobozzz
author_date: 2022-01-06 13:27:29+01:00
msg: Prevent caption listing of private videos
repository: repo_url: https://github.com/chocobozzz/peertube
repo_name: Chocobozzz/PeerTube
description: ActivityPub-federated video streaming platform using P2P directly in your web browser
repo_language: TypeScript
owner: chocobozzz
file_changes: filename: captions.ts
change_type: ModificationType.MODIFY
diff: @@ -48,7 +48,7 @@ export {
 // ---------------------------------------------------------------------------
 
 async function listVideoCaptions (req: express.Request, res: express.Response) {
-  const data = await VideoCaptionModel.listVideoCaptions(res.locals.videoId.id)
+  const data = await VideoCaptionModel.listVideoCaptions(res.locals.onlyVideo.id)
 
   return res.json(getFormattedObjects(data, data.length))
 }

programming_language: JavaScript
filename: files.ts
change_type: ModificationType.MODIFY
diff: @@ -10,13 +10,15 @@ import {
   ensureUserHasRight,
   videoFileMetadataGetValidator,
   videoFilesDeleteHLSValidator,
-  videoFilesDeleteWebTorrentValidator
+  videoFilesDeleteWebTorrentValidator,
+  videosGetValidator
 } from '../../../middlewares'
 
 const lTags = loggerTagsFactory('api', 'video')
 const filesRouter = express.Router()
 
 filesRouter.get('/:id/metadata/:videoFileId',
+  asyncMiddleware(videosGetValidator),
   asyncMiddleware(videoFileMetadataGetValidator),
   asyncMiddleware(getVideoFileMetadata)
 )

programming_language: JavaScript
filename: videos.ts
change_type: ModificationType.MODIFY
diff: @@ -1,16 +1,20 @@
-import { Response } from 'express'
+import { Request, Response } from 'express'
 import { loadVideo, VideoLoadType } from '@server/lib/model-loaders'
+import { authenticatePromiseIfNeeded } from '@server/middlewares/auth'
+import { VideoModel } from '@server/models/video/video'
 import { VideoChannelModel } from '@server/models/video/video-channel'
 import { VideoFileModel } from '@server/models/video/video-file'
 import {
   MUser,
   MUserAccountId,
+  MVideo,
   MVideoAccountLight,
   MVideoFormattableDetails,
   MVideoFullLight,
   MVideoId,
   MVideoImmutable,
-  MVideoThumbnail
+  MVideoThumbnail,
+  MVideoWithRights
 } from '@server/types/models'
 import { HttpStatusCode, UserRight } from '@shared/models'
 
@@ -89,6 +93,27 @@ async function doesVideoChannelOfAccountExist (channelId: number, user: MUserAcc
   return true
 }
 
+async function checkCanSeeVideoIfPrivate (req: Request, res: Response, video: MVideo, authenticateInQuery = false) {
+  if (!video.requiresAuth()) return true
+
+  const videoWithRights = await VideoModel.loadAndPopulateAccountAndServerAndTags(video.id)
+
+  return checkCanSeePrivateVideo(req, res, videoWithRights, authenticateInQuery)
+}
+
+async function checkCanSeePrivateVideo (req: Request, res: Response, video: MVideoWithRights, authenticateInQuery = false) {
+  await authenticatePromiseIfNeeded(req, res, authenticateInQuery)
+
+  const user = res.locals.oauth ? res.locals.oauth.token.User : null
+
+  // Only the owner or a user that have blocklist rights can see the video
+  if (!user || !user.canGetVideo(video)) {
+    return false
+  }
+
+  return true
+}
+
 function checkUserCanManageVideo (user: MUser, video: MVideoAccountLight, right: UserRight, res: Response, onlyOwned = true) {
   // Retrieve the user who did the request
   if (onlyOwned && video.isOwned() === false) {
@@ -120,5 +145,7 @@ export {
   doesVideoChannelOfAccountExist,
   doesVideoExist,
   doesVideoFileOfVideoExist,
-  checkUserCanManageVideo
+  checkUserCanManageVideo,
+  checkCanSeeVideoIfPrivate,
+  checkCanSeePrivateVideo
 }

programming_language: CoffeeScript
filename: video-captions.ts
change_type: ModificationType.MODIFY
diff: @@ -1,11 +1,18 @@
 import express from 'express'
 import { body, param } from 'express-validator'
-import { UserRight } from '../../../../shared'
+import { HttpStatusCode, UserRight } from '../../../../shared'
 import { isVideoCaptionFile, isVideoCaptionLanguageValid } from '../../../helpers/custom-validators/video-captions'
 import { cleanUpReqFiles } from '../../../helpers/express-utils'
 import { logger } from '../../../helpers/logger'
 import { CONSTRAINTS_FIELDS, MIMETYPES } from '../../../initializers/constants'
-import { areValidationErrors, checkUserCanManageVideo, doesVideoCaptionExist, doesVideoExist, isValidVideoIdParam } from '../shared'
+import {
+  areValidationErrors,
+  checkCanSeeVideoIfPrivate,
+  checkUserCanManageVideo,
+  doesVideoCaptionExist,
+  doesVideoExist,
+  isValidVideoIdParam
+} from '../shared'
 
 const addVideoCaptionValidator = [
   isValidVideoIdParam('videoId'),
@@ -64,7 +71,16 @@ const listVideoCaptionsValidator = [
     logger.debug('Checking listVideoCaptions parameters', { parameters: req.params })
 
     if (areValidationErrors(req, res)) return
-    if (!await doesVideoExist(req.params.videoId, res, 'id')) return
+    if (!await doesVideoExist(req.params.videoId, res, 'only-video')) return
+
+    const video = res.locals.onlyVideo
+
+    if (!await checkCanSeeVideoIfPrivate(req, res, video)) {
+      return res.fail({
+        status: HttpStatusCode.FORBIDDEN_403,
+        message: 'Cannot list captions of private/internal/blocklisted video'
+      })
+    }
 
     return next()
   }

programming_language: CoffeeScript
filename: videos.ts
change_type: ModificationType.MODIFY
diff: @@ -51,9 +51,9 @@ import { CONSTRAINTS_FIELDS, OVERVIEWS } from '../../../initializers/constants'
 import { isLocalVideoAccepted } from '../../../lib/moderation'
 import { Hooks } from '../../../lib/plugins/hooks'
 import { VideoModel } from '../../../models/video/video'
-import { authenticatePromiseIfNeeded } from '../../auth'
 import {
   areValidationErrors,
+  checkCanSeePrivateVideo,
   checkUserCanManageVideo,
   doesVideoChannelOfAccountExist,
   doesVideoExist,
@@ -317,19 +317,12 @@ const videosCustomGetValidator = (
 
       // Video private or blacklisted
       if (video.requiresAuth()) {
-        await authenticatePromiseIfNeeded(req, res, authenticateInQuery)
+        if (await checkCanSeePrivateVideo(req, res, video, authenticateInQuery)) return next()
 
-        const user = res.locals.oauth ? res.locals.oauth.token.User : null
-
-        // Only the owner or a user that have blocklist rights can see the video
-        if (!user || !user.canGetVideo(video)) {
-          return res.fail({
-            status: HttpStatusCode.FORBIDDEN_403,
-            message: 'Cannot get this private/internal or blocklisted video'
-          })
-        }
-
-        return next()
+        return res.fail({
+          status: HttpStatusCode.FORBIDDEN_403,
+          message: 'Cannot get this private/internal or blocklisted video'
+        })
       }
 
       // Video is public, anyone can access it

programming_language: CoffeeScript
filename: video-captions.ts
change_type: ModificationType.MODIFY
diff: @@ -11,7 +11,7 @@ import {
   PeerTubeServer,
   setAccessTokensToServers
 } from '@shared/extra-utils'
-import { HttpStatusCode, VideoCreateResult } from '@shared/models'
+import { HttpStatusCode, VideoCreateResult, VideoPrivacy } from '@shared/models'
 
 describe('Test video captions API validator', function () {
   const path = '/api/v1/videos/'
@@ -19,6 +19,7 @@ describe('Test video captions API validator', function () {
   let server: PeerTubeServer
   let userAccessToken: string
   let video: VideoCreateResult
+  let privateVideo: VideoCreateResult
 
   // ---------------------------------------------------------------
 
@@ -30,6 +31,7 @@ describe('Test video captions API validator', function () {
     await setAccessTokensToServers([ server ])
 
     video = await server.videos.upload()
+    privateVideo = await server.videos.upload({ attributes: { privacy: VideoPrivacy.PRIVATE } })
 
     {
       const user = {
@@ -204,8 +206,32 @@ describe('Test video captions API validator', function () {
       })
     })
 
+    it('Should fail with a private video without token', async function () {
+      await makeGetRequest({
+        url: server.url,
+        path: path + privateVideo.shortUUID + '/captions',
+        expectedStatus: HttpStatusCode.UNAUTHORIZED_401
+      })
+    })
+
+    it('Should fail with another user token', async function () {
+      await makeGetRequest({
+        url: server.url,
+        token: userAccessToken,
+        path: path + privateVideo.shortUUID + '/captions',
+        expectedStatus: HttpStatusCode.FORBIDDEN_403
+      })
+    })
+
     it('Should success with the correct parameters', async function () {
       await makeGetRequest({ url: server.url, path: path + video.shortUUID + '/captions', expectedStatus: HttpStatusCode.OK_200 })
+
+      await makeGetRequest({
+        url: server.url,
+        path: path + privateVideo.shortUUID + '/captions',
+        token: server.accessToken,
+        expectedStatus: HttpStatusCode.OK_200
+      })
     })
   })
 

programming_language: JavaScript