CVE-2022-0170: Improper Access Control (CWE-284)
value: peertube is vulnerable to Improper Access Control
CWE description: The product does not restrict or incorrectly restricts access to a resource from an unauthorized actor.
CVSS v3 base score: 4.3
Published: 2022-01-11T16:15Z
Repository: https://github.com/chocobozzz/peertube
Language: TypeScript
Files changed: server/middlewares/validators/videos/video-comments.ts, server/tests/api/check-params/video-comments.ts
Commit date: 2022-01-10 16:15:09 +0100
Commit message: Don't display comments of private/internal videos

Diff: diff --git a/server/middlewares/validators/videos/video-comments.ts b/server/middlewares/validators/videos/video-comments.ts
index 3ea8bdcbbe..04e7b69730 100644
--- a/server/middlewares/validators/videos/video-comments.ts
+++ b/server/middlewares/validators/videos/video-comments.ts
@@ -7,11 +7,18 @@ import { exists, isBooleanValid, isIdValid, toBooleanOrNull } from '../../../hel
 import { isValidVideoCommentText } from '../../../helpers/custom-validators/video-comments'
 import { logger } from '../../../helpers/logger'
 import { AcceptResult, isLocalVideoCommentReplyAccepted, isLocalVideoThreadAccepted } from '../../../lib/moderation'
 import { Hooks } from '../../../lib/plugins/hooks'
 import { MCommentOwnerVideoReply, MVideo, MVideoFullLight } from '../../../types/models/video'
-import { areValidationErrors, doesVideoCommentExist, doesVideoCommentThreadExist, doesVideoExist, isValidVideoIdParam } from '../shared'
+import {
+  areValidationErrors,
+  checkCanSeeVideoIfPrivate,
+  doesVideoCommentExist,
+  doesVideoCommentThreadExist,
+  doesVideoExist,
+  isValidVideoIdParam
+} from '../shared'
 
 const listVideoCommentsValidator = [
   query('isLocal')
   .optional()
   .customSanitizer(toBooleanOrNull)
@@ -46,10 +53,17 @@ const listVideoCommentThreadsValidator = [
     logger.debug('Checking listVideoCommentThreads parameters.', { parameters: req.params })
 
     if (areValidationErrors(req, res)) return
     if (!await doesVideoExist(req.params.videoId, res, 'only-video')) return
 
+    if (!await checkCanSeeVideoIfPrivate(req, res, res.locals.onlyVideo)) {
+      return res.fail({
+        status: HttpStatusCode.FORBIDDEN_403,
+        message: 'Cannot list comments of private/internal/blocklisted video'
+      })
+    }
+
     return next()
   }
 ]
 
 const listVideoThreadCommentsValidator = [
@@ -63,10 +77,17 @@ const listVideoThreadCommentsValidator = [
 
     if (areValidationErrors(req, res)) return
     if (!await doesVideoExist(req.params.videoId, res, 'only-video')) return
     if (!await doesVideoCommentThreadExist(req.params.threadId, res.locals.onlyVideo, res)) return
 
+    if (!await checkCanSeeVideoIfPrivate(req, res, res.locals.onlyVideo)) {
+      return res.fail({
+        status: HttpStatusCode.FORBIDDEN_403,
+        message: 'Cannot list threads of private/internal/blocklisted video'
+      })
+    }
+
     return next()
   }
 ]
 
 const addVideoCommentThreadValidator = [
diff --git a/server/tests/api/check-params/video-comments.ts b/server/tests/api/check-params/video-comments.ts
index 2d9ee1e0d2..8d63fe70c7 100644
--- a/server/tests/api/check-params/video-comments.ts
+++ b/server/tests/api/check-params/video-comments.ts
@@ -12,11 +12,11 @@ import {
   makeGetRequest,
   makePostBodyRequest,
   PeerTubeServer,
   setAccessTokensToServers
 } from '@shared/extra-utils'
-import { HttpStatusCode, VideoCreateResult } from '@shared/models'
+import { HttpStatusCode, VideoCreateResult, VideoPrivacy } from '@shared/models'
 
 const expect = chai.expect
 
 describe('Test video comments API validator', function () {
   let pathThread: string
@@ -24,10 +24,12 @@ describe('Test video comments API validator', function () {
   let server: PeerTubeServer
   let video: VideoCreateResult
   let userAccessToken: string
   let userAccessToken2: string
   let commentId: number
+  let privateCommentId: number
+  let privateVideo: VideoCreateResult
 
   // ---------------------------------------------------------------
 
   before(async function () {
     this.timeout(30000)
@@ -39,16 +41,25 @@ describe('Test video comments API validator', function () {
     {
       video = await server.videos.upload({ attributes: {} })
       pathThread = '/api/v1/videos/' + video.uuid + '/comment-threads'
     }
 
+    {
+      privateVideo = await server.videos.upload({ attributes: { privacy: VideoPrivacy.PRIVATE } })
+    }
+
     {
       const created = await server.comments.createThread({ videoId: video.uuid, text: 'coucou' })
       commentId = created.id
       pathComment = '/api/v1/videos/' + video.uuid + '/comments/' + commentId
     }
 
+    {
+      const created = await server.comments.createThread({ videoId: privateVideo.uuid, text: 'coucou' })
+      privateCommentId = created.id
+    }
+
     {
       const user = { username: 'user1', password: 'my super password' }
       await server.users.create({ username: user.username, password: user.password })
       userAccessToken = await server.login.getAccessToken(user)
     }
@@ -78,10 +89,36 @@ describe('Test video comments API validator', function () {
         url: server.url,
         path: '/api/v1/videos/ba708d62-e3d7-45d9-9d73-41b9097cc02d/comment-threads',
         expectedStatus: HttpStatusCode.NOT_FOUND_404
       })
     })
+
+    it('Should fail with a private video without token', async function () {
+      await makeGetRequest({
+        url: server.url,
+        path: '/api/v1/videos/' + privateVideo.shortUUID + '/comment-threads',
+        expectedStatus: HttpStatusCode.UNAUTHORIZED_401
+      })
+    })
+
+    it('Should fail with another user token', async function () {
+      await makeGetRequest({
+        url: server.url,
+        token: userAccessToken,
+        path: '/api/v1/videos/' + privateVideo.shortUUID + '/comment-threads',
+        expectedStatus: HttpStatusCode.FORBIDDEN_403
+      })
+    })
+
+    it('Should succeed with the correct params', async function () {
+      await makeGetRequest({
+        url: server.url,
+        token: server.accessToken,
+        path: '/api/v1/videos/' + privateVideo.shortUUID + '/comment-threads',
+        expectedStatus: HttpStatusCode.OK_200
+      })
+    })
   })
 
   describe('When listing comments of a thread', function () {
     it('Should fail with an incorrect video', async function () {
       await makeGetRequest({
@@ -97,11 +134,35 @@ describe('Test video comments API validator', function () {
         path: '/api/v1/videos/' + video.shortUUID + '/comment-threads/156',
         expectedStatus: HttpStatusCode.NOT_FOUND_404
       })
     })
 
+    it('Should fail with a private video without token', async function () {
+      await makeGetRequest({
+        url: server.url,
+        path: '/api/v1/videos/' + privateVideo.shortUUID + '/comment-threads/' + privateCommentId,
+        expectedStatus: HttpStatusCode.UNAUTHORIZED_401
+      })
+    })
+
+    it('Should fail with another user token', async function () {
+      await makeGetRequest({
+        url: server.url,
+        token: userAccessToken,
+        path: '/api/v1/videos/' + privateVideo.shortUUID + '/comment-threads/' + privateCommentId,
+        expectedStatus: HttpStatusCode.FORBIDDEN_403
+      })
+    })
+
     it('Should success with the correct params', async function () {
+      await makeGetRequest({
+        url: server.url,
+        token: server.accessToken,
+        path: '/api/v1/videos/' + privateVideo.shortUUID + '/comment-threads/' + privateCommentId,
+        expectedStatus: HttpStatusCode.OK_200
+      })
+
       await makeGetRequest({
         url: server.url,
         path: '/api/v1/videos/' + video.shortUUID + '/comment-threads/' + commentId,
         expectedStatus: HttpStatusCode.OK_200
       })

Vulnerable code: import { areValidationErrors, doesVideoCommentExist, doesVideoCommentThreadExist, doesVideoExist, isValidVideoIdParam } from '../shared'
import { HttpStatusCode, VideoCreateResult } from '@shared/models'
Fixed code: import {
  areValidationErrors,
  checkCanSeeVideoIfPrivate,
  doesVideoCommentExist,
  doesVideoCommentThreadExist,
  doesVideoExist,
  isValidVideoIdParam
} from '../shared'
    if (!await checkCanSeeVideoIfPrivate(req, res, res.locals.onlyVideo)) {
      return res.fail({
        status: HttpStatusCode.FORBIDDEN_403,
        message: 'Cannot list comments of private/internal/blocklisted video'
      })
    }

    if (!await checkCanSeeVideoIfPrivate(req, res, res.locals.onlyVideo)) {
      return res.fail({
        status: HttpStatusCode.FORBIDDEN_403,
        message: 'Cannot list threads of private/internal/blocklisted video'
      })
    }

import { HttpStatusCode, VideoCreateResult, VideoPrivacy } from '@shared/models'
  let privateCommentId: number
  let privateVideo: VideoCreateResult
    {
      privateVideo = await server.videos.upload({ attributes: { privacy: VideoPrivacy.PRIVATE } })
    }

    {
      const created = await server.comments.createThread({ videoId: privateVideo.uuid, text: 'coucou' })
      privateCommentId = created.id
    }


    it('Should fail with a private video without token', async function () {
      await makeGetRequest({
        url: server.url,
        path: '/api/v1/videos/' + privateVideo.shortUUID + '/comment-threads',
        expectedStatus: HttpStatusCode.UNAUTHORIZED_401
      })
    })

    it('Should fail with another user token', async function () {
      await makeGetRequest({
        url: server.url,
        token: userAccessToken,
        path: '/api/v1/videos/' + privateVideo.shortUUID + '/comment-threads',
        expectedStatus: HttpStatusCode.FORBIDDEN_403
      })
    })

    it('Should succeed with the correct params', async function () {
      await makeGetRequest({
        url: server.url,
        token: server.accessToken,
        path: '/api/v1/videos/' + privateVideo.shortUUID + '/comment-threads',
        expectedStatus: HttpStatusCode.OK_200
      })
    })
    it('Should fail with a private video without token', async function () {
      await makeGetRequest({
        url: server.url,
        path: '/api/v1/videos/' + privateVideo.shortUUID + '/comment-threads/' + privateCommentId,
        expectedStatus: HttpStatusCode.UNAUTHORIZED_401
      })
    })

    it('Should fail with another user token', async function () {
      await makeGetRequest({
        url: server.url,
        token: userAccessToken,
        path: '/api/v1/videos/' + privateVideo.shortUUID + '/comment-threads/' + privateCommentId,
        expectedStatus: HttpStatusCode.FORBIDDEN_403
      })
    })

      await makeGetRequest({
        url: server.url,
        token: server.accessToken,
        path: '/api/v1/videos/' + privateVideo.shortUUID + '/comment-threads/' + privateCommentId,
        expectedStatus: HttpStatusCode.OK_200
      })