CVE-2022-0881 (published 2022-03-09T09:15Z)

Insecure Storage of Sensitive Information in GitHub repository chocobozzz/peertube prior to 4.1.1.

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

Weakness information:
cwe_id: CWE-922
cwe_name: Insecure Storage of Sensitive Information
description: The product stores sensitive information without properly limiting read or write access by unauthorized actors.
extended_description: If read access is not properly restricted, then attackers can steal the sensitive information. If write access is not properly restricted, then attackers can modify and possibly delete the data, causing incorrect results and possibly a denial of service.
         

Fix information:
repo_url: https://github.com/chocobozzz/peertube
commit_details: repo_url: https://github.com/chocobozzz/peertube
author: Chocobozzz
author_date: 2022-03-07 17:16:54+01:00
msg: Remove exif tags when processing images
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: action.yml
change_type: ModificationType.MODIFY
diff: @@ -8,7 +8,7 @@ runs:
     - name: Setup system dependencies
       shell: bash
       run: |
-        sudo apt-get install postgresql-client-common redis-tools parallel
+        sudo apt-get install postgresql-client-common redis-tools parallel libimage-exiftool-perl
         wget --quiet --no-check-certificate "https://download.cpy.re/ffmpeg/ffmpeg-release-4.3.1-64bit-static.tar.xz"
         tar xf ffmpeg-release-4.3.1-64bit-static.tar.xz
         mkdir -p $HOME/bin

programming_language: Shell
filename: image-utils.ts
change_type: ModificationType.MODIFY
diff: @@ -80,6 +80,8 @@ async function autoResize (options: {
   const sourceIsPortrait = sourceImage.getWidth() < sourceImage.getHeight()
   const destIsPortraitOrSquare = newSize.width <= newSize.height
 
+  removeExif(sourceImage)
+
   if (sourceIsPortrait && !destIsPortraitOrSquare) {
     const baseImage = sourceImage.cloneQuiet().cover(newSize.width, newSize.height)
                                               .color([ { apply: 'shade', params: [ 50 ] } ])
@@ -106,6 +108,7 @@ function skipProcessing (options: {
   const { sourceImage, newSize, imageBytes, inputExt, outputExt } = options
   const { width, height } = newSize
 
+  if (hasExif(sourceImage)) return false
   if (sourceImage.getWidth() > width || sourceImage.getHeight() > height) return false
   if (inputExt !== outputExt) return false
 
@@ -116,3 +119,11 @@ function skipProcessing (options: {
 
   return imageBytes <= 15 * kB
 }
+
+function hasExif (image: Jimp) {
+  return !!(image.bitmap as any).exifBuffer
+}
+
+function removeExif (image: Jimp) {
+  (image.bitmap as any).exifBuffer = null
+}

programming_language: TypeScript
filename: image.ts
change_type: ModificationType.MODIFY
diff: @@ -4,6 +4,7 @@ import 'mocha'
 import { expect } from 'chai'
 import { readFile, remove } from 'fs-extra'
 import { join } from 'path'
+import { execPromise } from '@server/helpers/core-utils'
 import { buildAbsoluteFixturePath, root } from '@shared/core-utils'
 import { processImage } from '../../../server/helpers/image-utils'
 
@@ -20,40 +21,77 @@ async function checkBuffers (path1: string, path2: string, equals: boolean) {
   }
 }
 
+async function hasTitleExif (path: string) {
+  const result = JSON.parse(await execPromise(`exiftool -json ${path}`))
+
+  return result[0]?.Title === 'should be removed'
+}
+
 describe('Image helpers', function () {
   const imageDestDir = join(root(), 'test-images')
-  const imageDest = join(imageDestDir, 'test.jpg')
+
+  const imageDestJPG = join(imageDestDir, 'test.jpg')
+  const imageDestPNG = join(imageDestDir, 'test.png')
+
   const thumbnailSize = { width: 223, height: 122 }
 
   it('Should skip processing if the source image is okay', async function () {
     const input = buildAbsoluteFixturePath('thumbnail.jpg')
-    await processImage(input, imageDest, thumbnailSize, true)
+    await processImage(input, imageDestJPG, thumbnailSize, true)
 
-    await checkBuffers(input, imageDest, true)
+    await checkBuffers(input, imageDestJPG, true)
   })
 
   it('Should not skip processing if the source image does not have the appropriate extension', async function () {
     const input = buildAbsoluteFixturePath('thumbnail.png')
-    await processImage(input, imageDest, thumbnailSize, true)
+    await processImage(input, imageDestJPG, thumbnailSize, true)
 
-    await checkBuffers(input, imageDest, false)
+    await checkBuffers(input, imageDestJPG, false)
   })
 
   it('Should not skip processing if the source image does not have the appropriate size', async function () {
     const input = buildAbsoluteFixturePath('preview.jpg')
-    await processImage(input, imageDest, thumbnailSize, true)
+    await processImage(input, imageDestJPG, thumbnailSize, true)
 
-    await checkBuffers(input, imageDest, false)
+    await checkBuffers(input, imageDestJPG, false)
   })
 
   it('Should not skip processing if the source image does not have the appropriate size', async function () {
     const input = buildAbsoluteFixturePath('thumbnail-big.jpg')
-    await processImage(input, imageDest, thumbnailSize, true)
+    await processImage(input, imageDestJPG, thumbnailSize, true)
+
+    await checkBuffers(input, imageDestJPG, false)
+  })
+
+  it('Should strip exif for a jpg file that can not be copied', async function () {
+    const input = buildAbsoluteFixturePath('exif.jpg')
+    expect(await hasTitleExif(input)).to.be.true
+
+    await processImage(input, imageDestJPG, { width: 100, height: 100 }, true)
+    await checkBuffers(input, imageDestJPG, false)
+
+    expect(await hasTitleExif(imageDestJPG)).to.be.false
+  })
+
+  it('Should strip exif for a jpg file that could be copied', async function () {
+    const input = buildAbsoluteFixturePath('exif.jpg')
+    expect(await hasTitleExif(input)).to.be.true
+
+    await processImage(input, imageDestJPG, thumbnailSize, true)
+    await checkBuffers(input, imageDestJPG, false)
+
+    expect(await hasTitleExif(imageDestJPG)).to.be.false
+  })
+
+  it('Should strip exif for png', async function () {
+    const input = buildAbsoluteFixturePath('exif.png')
+    expect(await hasTitleExif(input)).to.be.true
 
-    await checkBuffers(input, imageDest, false)
+    await processImage(input, imageDestPNG, thumbnailSize, true)
+    expect(await hasTitleExif(imageDestPNG)).to.be.false
   })
 
   after(async function () {
-    await remove(imageDest)
+    await remove(imageDestDir)
   })
 })

programming_language: TypeScript
filename: tests.md
change_type: ModificationType.MODIFY
diff: @@ -31,6 +31,12 @@ $ sudo docker run -p 9444:9000 chocobozzz/s3-ninja
 $ sudo docker run -p 10389:10389 chocobozzz/docker-test-openldap
 ```
 
+Ensure you also have these commands:
+
+```
+$ exiftool --help
+```
+
 ### Test
 
 To run all test suites:
@@ -39,7 +45,7 @@ To run all test suites:
 $ npm run test # See scripts/test.sh to run a particular suite
 ```
 
-Most of tests can be runned using:
+Most of tests can be run using:
 
 ```bash
 TS_NODE_TRANSPILE_ONLY=true npm run mocha -- --timeout 30000 --exit -r ts-node/register -r tsconfig-paths/register --bail server/tests/api/videos/video-transcoder.ts

programming_language: Markdown