CVE-2022-0881: Insecure Storage of Sensitive Information (CWE-922)
value: Insecure Storage of Sensitive Information in GitHub repository chocobozzz/peertube prior to 4.1.1.
CWE description: The product stores sensitive information without properly limiting read or write access by unauthorized actors.
CVSS v3 base score: 6.5
Published: 2022-03-09T09:15Z
Repository: https://github.com/chocobozzz/peertube
Language: TypeScript
Files changed: .github/actions/reusable-prepare-peertube-run/action.yml, server/helpers/image-utils.ts, server/tests/helpers/image.ts, support/doc/development/tests.md
Commit date: 2022-03-07 17:23:12 +0100
Commit message: Remove exif tags when processing images

Diff: diff --git a/.github/actions/reusable-prepare-peertube-run/action.yml b/.github/actions/reusable-prepare-peertube-run/action.yml
index 1a6cd2cfde..aa5b897c9f 100644
--- a/.github/actions/reusable-prepare-peertube-run/action.yml
+++ b/.github/actions/reusable-prepare-peertube-run/action.yml
@@ -6,11 +6,11 @@ runs:
 
   steps:
     - 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
         cp ffmpeg-*/{ffmpeg,ffprobe} $HOME/bin
         echo "$HOME/bin" >> $GITHUB_PATH
diff --git a/server/helpers/image-utils.ts b/server/helpers/image-utils.ts
index b174ae4369..28d8fff4cb 100644
--- a/server/helpers/image-utils.ts
+++ b/server/helpers/image-utils.ts
@@ -78,10 +78,12 @@ async function autoResize (options: {
 
   // Portrait mode targetting a landscape, apply some effect on the image
   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 ] } ])
 
     const topImage = sourceImage.cloneQuiet().contain(newSize.width, newSize.height)
@@ -104,15 +106,24 @@ function skipProcessing (options: {
   outputExt: string
 }) {
   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
 
   const kB = 1000
 
   if (height >= 1000) return imageBytes <= 200 * kB
   if (height >= 500) return imageBytes <= 100 * kB
 
   return imageBytes <= 15 * kB
 }
+
+function hasExif (image: Jimp) {
+  return !!(image.bitmap as any).exifBuffer
+}
+
+function removeExif (image: Jimp) {
+  (image.bitmap as any).exifBuffer = null
+}
diff --git a/server/tests/fixtures/exif.jpg b/server/tests/fixtures/exif.jpg
new file mode 100644
index 0000000000..2997b38e99
Binary files /dev/null and b/server/tests/fixtures/exif.jpg differ
diff --git a/server/tests/fixtures/exif.png b/server/tests/fixtures/exif.png
new file mode 100644
index 0000000000..a1a0113f8e
Binary files /dev/null and b/server/tests/fixtures/exif.png differ
diff --git a/server/tests/helpers/image.ts b/server/tests/helpers/image.ts
index 64bd373cc9..475ca8fb25 100644
--- a/server/tests/helpers/image.ts
+++ b/server/tests/helpers/image.ts
@@ -2,10 +2,11 @@
 
 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'
 
 async function checkBuffers (path1: string, path2: string, equals: boolean) {
   const [ buf1, buf2 ] = await Promise.all([
@@ -18,42 +19,79 @@ async function checkBuffers (path1: string, path2: string, equals: boolean) {
   } else {
     expect(buf1.equals(buf2)).to.be.false
   }
 }
 
+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)
   })
 })
diff --git a/support/doc/development/tests.md b/support/doc/development/tests.md
index 02fc411475..47602156c8 100644
--- a/support/doc/development/tests.md
+++ b/support/doc/development/tests.md
@@ -29,19 +29,25 @@ Run docker containers needed by some test files:
 ```bash
 $ 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:
 
 ```bash
 $ 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
 ```
 

Vulnerable code:         sudo apt-get install postgresql-client-common redis-tools parallel
  const imageDest = join(imageDestDir, 'test.jpg')
    await processImage(input, imageDest, thumbnailSize, true)
    await checkBuffers(input, imageDest, true)
    await processImage(input, imageDest, thumbnailSize, true)
    await checkBuffers(input, imageDest, false)
    await processImage(input, imageDest, thumbnailSize, true)
    await checkBuffers(input, imageDest, false)
    await processImage(input, imageDest, thumbnailSize, true)
    await checkBuffers(input, imageDest, false)
    await remove(imageDest)
Most of tests can be runned using:
Fixed code:         sudo apt-get install postgresql-client-common redis-tools parallel libimage-exiftool-perl
  removeExif(sourceImage)

  if (hasExif(sourceImage)) return false

function hasExif (image: Jimp) {
  return !!(image.bitmap as any).exifBuffer
}

function removeExif (image: Jimp) {
  (image.bitmap as any).exifBuffer = null
}
import { execPromise } from '@server/helpers/core-utils'
async function hasTitleExif (path: string) {
  const result = JSON.parse(await execPromise(`exiftool -json ${path}`))

  return result[0]?.Title === 'should be removed'
}


  const imageDestJPG = join(imageDestDir, 'test.jpg')
  const imageDestPNG = join(imageDestDir, 'test.png')

    await processImage(input, imageDestJPG, thumbnailSize, true)
    await checkBuffers(input, imageDestJPG, true)
    await processImage(input, imageDestJPG, thumbnailSize, true)
    await checkBuffers(input, imageDestJPG, false)
    await processImage(input, imageDestJPG, thumbnailSize, true)
    await checkBuffers(input, imageDestJPG, false)
    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 processImage(input, imageDestPNG, thumbnailSize, true)
    expect(await hasTitleExif(imageDestPNG)).to.be.false
    await remove(imageDestDir)
Ensure you also have these commands:

```
$ exiftool --help
```

Most of tests can be run using: