CVE-2022-25851: Loop with Unreachable Exit Condition ('Infinite Loop') (CWE-835)
value: The package jpeg-js before 0.4.4 are vulnerable to Denial of Service (DoS) where a particular piece of input will cause to enter an infinite loop and never return.
CWE description: The product contains an iteration or loop with an exit condition that cannot be reached, i.e., an infinite loop.
CVSS v3 base score: 7.5
Published: 2022-06-10T20:15Z
Repository: https://github.com/jpeg-js/jpeg-js
Language: JavaScript
Files changed: lib/decoder.js, test/index.js
Commit date: 2022-06-03 09:40:52 -0500
Commit message: fix: validate sampling factors (#106)


Diff: diff --git a/lib/decoder.js b/lib/decoder.js
index d61a8b5..1982481 100644
--- a/lib/decoder.js
+++ b/lib/decoder.js
@@ -574,11 +574,13 @@ var JpegImage = (function jpegImage() {
         var array = data.subarray(offset, offset + length - 2);
         offset += array.length;
         return array;
       }
       function prepareComponents(frame) {
-        var maxH = 0, maxV = 0;
+        // According to the JPEG standard, the sampling factor must be between 1 and 4
+        // See https://github.com/libjpeg-turbo/libjpeg-turbo/blob/9abeff46d87bd201a952e276f3e4339556a403a3/libjpeg.txt#L1138-L1146
+        var maxH = 1, maxV = 1;
         var component, componentId;
         for (componentId in frame.components) {
           if (frame.components.hasOwnProperty(componentId)) {
             component = frame.components[componentId];
             if (maxH < component.h) maxH = component.h;
@@ -743,10 +745,15 @@ var JpegImage = (function jpegImage() {
             for (i = 0; i < componentsCount; i++) {
               componentId = data[offset];
               var h = data[offset + 1] >> 4;
               var v = data[offset + 1] & 15;
               var qId = data[offset + 2];
+
+              if ( h <= 0 || v <= 0 ) {
+                throw new Error('Invalid sampling factor, expected values above 0');
+              }
+
               frame.componentsOrder.push(componentId);
               frame.components[componentId] = {
                 h: h,
                 v: v,
                 quantizationIdx: qId
diff --git a/test/index.js b/test/index.js
index fd1339a..7f0edbb 100644
--- a/test/index.js
+++ b/test/index.js
@@ -5,14 +5,16 @@ var fs = require('fs'),
 
 function fixture(name) {
   return fs.readFileSync(path.join(__dirname, 'fixtures', name));
 }
 
-const SUPER_LARGE_JPEG_BASE64 =
-  '/9j/wJ39sP//DlKWvX+7xPlXkJa9f7v8DoDVAAD//zb6QAEAI2cBv3P/r4ADpX8Jf14AAAAAgCPE+VeQlr1/uwCAAAAVALNOjAGP2lIS';
+const SUPER_LARGE_JPEG_BASE64 = '/9j/wfFRBf//BdgC/9p/2P/E4d4=';
+
+const SUPER_LARGE_RESOLUTION_JPEG_BASE64 = '/9j/wfFR2PDh3g==';
 
 const SUPER_LARGE_JPEG_BUFFER = Buffer.from(SUPER_LARGE_JPEG_BASE64, 'base64');
+const SUPER_LARGE_RESOLUTION_JPEG_BUFFER = Buffer.from(SUPER_LARGE_RESOLUTION_JPEG_BASE64, 'base64');
 
 it('should be able read image with a bad e1 marker not preceeded by ff', function () {
     var jpegData = fixture('table-with-bad-e1.jpg');
     var rawImageData = jpeg.decode(jpegData);
     var expected = fixture('table-with-good-e1.jpg');
@@ -272,12 +274,12 @@ it('should be able to decode large images within memory limits', () => {
   expect(rawImageData.height).toEqual(6000);
 }, 30000);
 
 // See https://github.com/eugeneware/jpeg-js/issues/53
 it('should limit resolution exposure', function () {
-  expect(() => jpeg.decode(SUPER_LARGE_JPEG_BUFFER)).toThrow(
-    'maxResolutionInMP limit exceeded by 141MP',
+  expect(() => jpeg.decode(SUPER_LARGE_RESOLUTION_JPEG_BUFFER)).toThrow(
+    'maxResolutionInMP limit exceeded by 3405MP',
   );
 });
 
 it('should limit memory exposure', function () {
   expect(() => jpeg.decode(SUPER_LARGE_JPEG_BUFFER, {maxResolutionInMP: 500})).toThrow(
@@ -286,5 +288,11 @@ it('should limit memory exposure', function () {
 
   // Make sure the limit resets each decode.
   var jpegData = fixture('grumpycat.jpg');
   expect(() => jpeg.decode(jpegData)).not.toThrow();
 }, 30000);
+
+// See https://github.com/jpeg-js/jpeg-js/issues/105
+it('invalid sampling factor should error out', function () {
+  expect(() => jpeg.decode(Buffer.from('/9j/wfFR2AD/UdgA/9r/3g==', 'base64')).toThrow(
+    'Invalid sampling factor, expected values above 0'))
+});

Vulnerable code:         var maxH = 0, maxV = 0;
const SUPER_LARGE_JPEG_BASE64 =
  '/9j/wJ39sP//DlKWvX+7xPlXkJa9f7v8DoDVAAD//zb6QAEAI2cBv3P/r4ADpX8Jf14AAAAAgCPE+VeQlr1/uwCAAAAVALNOjAGP2lIS';
  expect(() => jpeg.decode(SUPER_LARGE_JPEG_BUFFER)).toThrow(
    'maxResolutionInMP limit exceeded by 141MP',
Fixed code:         // According to the JPEG standard, the sampling factor must be between 1 and 4
        // See https://github.com/libjpeg-turbo/libjpeg-turbo/blob/9abeff46d87bd201a952e276f3e4339556a403a3/libjpeg.txt#L1138-L1146
        var maxH = 1, maxV = 1;

              if ( h <= 0 || v <= 0 ) {
                throw new Error('Invalid sampling factor, expected values above 0');
              }

const SUPER_LARGE_JPEG_BASE64 = '/9j/wfFRBf//BdgC/9p/2P/E4d4=';

const SUPER_LARGE_RESOLUTION_JPEG_BASE64 = '/9j/wfFR2PDh3g==';
const SUPER_LARGE_RESOLUTION_JPEG_BUFFER = Buffer.from(SUPER_LARGE_RESOLUTION_JPEG_BASE64, 'base64');
  expect(() => jpeg.decode(SUPER_LARGE_RESOLUTION_JPEG_BUFFER)).toThrow(
    'maxResolutionInMP limit exceeded by 3405MP',

// See https://github.com/jpeg-js/jpeg-js/issues/105
it('invalid sampling factor should error out', function () {
  expect(() => jpeg.decode(Buffer.from('/9j/wfFR2AD/UdgA/9r/3g==', 'base64')).toThrow(
    'Invalid sampling factor, expected values above 0'))
});