Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 135705b

Browse files
committed
feat: add memory and resolution limits
BREAKING CHANGE: images larger than 100 megapixels or requiring more than 512MB of memory to decode will throw unless `maxMemoryInMB` and `maxResolutionInMP` options are increased
1 parent a2c93e0 commit 135705b

File tree

3 files changed

+81
-1
lines changed

3 files changed

+81
-1
lines changed

lib/decoder.js

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,7 @@ var JpegImage = (function jpegImage() {
359359
var blocksPerLine = component.blocksPerLine;
360360
var blocksPerColumn = component.blocksPerColumn;
361361
var samplesPerLine = blocksPerLine << 3;
362+
// Only 1 used per invocation of this function and garbage collected after invocation, so no need to account for its memory footprint.
362363
var R = new Int32Array(64), r = new Uint8Array(64);
363364

364365
// A port of poppler's IDCT method which in turn is taken from:
@@ -521,6 +522,8 @@ var JpegImage = (function jpegImage() {
521522
}
522523
}
523524

525+
requestMemoryAllocation(samplesPerLine * blocksPerColumn * 8);
526+
524527
var i, j;
525528
for (var blockRow = 0; blockRow < blocksPerColumn; blockRow++) {
526529
var scanLine = blockRow << 3;
@@ -559,6 +562,7 @@ var JpegImage = (function jpegImage() {
559562
xhr.send(null);
560563
},
561564
parse: function parse(data) {
565+
var maxResolutionInPixels = this.opts.maxResolutionInMP * 1000 * 1000;
562566
var offset = 0, length = data.length;
563567
function readUint16() {
564568
var value = (data[offset] << 8) | data[offset + 1];
@@ -590,7 +594,12 @@ var JpegImage = (function jpegImage() {
590594
var blocksPerColumn = Math.ceil(Math.ceil(frame.scanLines / 8) * component.v / maxV);
591595
var blocksPerLineForMcu = mcusPerLine * component.h;
592596
var blocksPerColumnForMcu = mcusPerColumn * component.v;
597+
var blocksToAllocate = blocksPerColumnForMcu * blocksPerLineForMcu;
593598
var blocks = [];
599+
600+
// Each block is a Int32Array of length 64 (4 x 64 = 256 bytes)
601+
requestMemoryAllocation(blocksToAllocate * 256);
602+
594603
for (var i = 0; i < blocksPerColumnForMcu; i++) {
595604
var row = [];
596605
for (var j = 0; j < blocksPerLineForMcu; j++)
@@ -685,6 +694,7 @@ var JpegImage = (function jpegImage() {
685694
var quantizationTablesEnd = quantizationTablesLength + offset - 2;
686695
while (offset < quantizationTablesEnd) {
687696
var quantizationTableSpec = data[offset++];
697+
requestMemoryAllocation(64 * 4);
688698
var tableData = new Int32Array(64);
689699
if ((quantizationTableSpec >> 4) === 0) { // 8 bit values
690700
for (j = 0; j < 64; j++) {
@@ -714,6 +724,13 @@ var JpegImage = (function jpegImage() {
714724
frame.samplesPerLine = readUint16();
715725
frame.components = {};
716726
frame.componentsOrder = [];
727+
728+
var pixelsInFrame = frame.scanLines * frame.samplesPerLine;
729+
if (pixelsInFrame > maxResolutionInPixels) {
730+
var exceededAmount = Math.ceil((pixelsInFrame - maxResolutionInPixels) / 1e6);
731+
throw new Error(`maxResolutionInMP limit exceeded by ${exceededAmount}MP`);
732+
}
733+
717734
var componentsCount = data[offset++], componentId;
718735
var maxH = 0, maxV = 0;
719736
for (i = 0; i < componentsCount; i++) {
@@ -739,8 +756,10 @@ var JpegImage = (function jpegImage() {
739756
var huffmanTableSpec = data[offset++];
740757
var codeLengths = new Uint8Array(16);
741758
var codeLengthSum = 0;
742-
for (j = 0; j < 16; j++, offset++)
759+
for (j = 0; j < 16; j++, offset++) {
743760
codeLengthSum += (codeLengths[j] = data[offset]);
761+
}
762+
requestMemoryAllocation(16 + codeLengthSum);
744763
var huffmanValues = new Uint8Array(codeLengthSum);
745764
for (j = 0; j < codeLengthSum; j++, offset++)
746765
huffmanValues[j] = data[offset];
@@ -832,6 +851,7 @@ var JpegImage = (function jpegImage() {
832851
var Y, Cb, Cr, K, C, M, Ye, R, G, B;
833852
var colorTransform;
834853
var dataLength = width * height * this.components.length;
854+
requestMemoryAllocation(dataLength);
835855
var data = new Uint8Array(dataLength);
836856
switch (this.components.length) {
837857
case 1:
@@ -1009,6 +1029,31 @@ var JpegImage = (function jpegImage() {
10091029
}
10101030
};
10111031

1032+
1033+
// We cap the amount of memory used by jpeg-js to avoid unexpected OOMs from untrusted content.
1034+
var totalBytesAllocated = 0;
1035+
var maxMemoryUsageBytes = 0;
1036+
function requestMemoryAllocation(increaseAmount = 0) {
1037+
var totalMemoryImpactBytes = totalBytesAllocated + increaseAmount;
1038+
if (totalMemoryImpactBytes > maxMemoryUsageBytes) {
1039+
var exceededAmount = Math.ceil((totalMemoryImpactBytes - maxMemoryUsageBytes) / 1024 / 1024);
1040+
throw new Error(`maxMemoryUsageInMB limit exceeded by at least ${exceededAmount}MB`);
1041+
}
1042+
1043+
totalBytesAllocated = totalMemoryImpactBytes;
1044+
}
1045+
1046+
constructor.resetMaxMemoryUsage = function (maxMemoryUsageBytes_) {
1047+
totalBytesAllocated = 0;
1048+
maxMemoryUsageBytes = maxMemoryUsageBytes_;
1049+
};
1050+
1051+
constructor.getBytesAllocated = function () {
1052+
return totalBytesAllocated;
1053+
};
1054+
1055+
constructor.requestMemoryAllocation = requestMemoryAllocation;
1056+
10121057
return constructor;
10131058
})();
10141059

@@ -1026,18 +1071,24 @@ function decode(jpegData, userOpts = {}) {
10261071
colorTransform: undefined,
10271072
formatAsRGBA: true,
10281073
tolerantDecoding: false,
1074+
maxResolutionInMP: 100, // Don't decode more than 100 megapixels
1075+
maxMemoryUsageInMB: 512, // Don't decode if memory footprint is more than 512MB
10291076
};
10301077

10311078
var opts = {...defaultOpts, ...userOpts};
10321079
var arr = new Uint8Array(jpegData);
10331080
var decoder = new JpegImage();
10341081
decoder.opts = opts;
1082+
// If this constructor ever supports async decoding this will need to be done differently.
1083+
// Until then, treating as singleton limit is fine.
1084+
JpegImage.resetMaxMemoryUsage(opts.maxMemoryUsageInMB * 1024 * 1024);
10351085
decoder.parse(arr);
10361086
decoder.colorTransform = opts.colorTransform;
10371087

10381088
var channels = (opts.formatAsRGBA) ? 4 : 3;
10391089
var bytesNeeded = decoder.width * decoder.height * channels;
10401090
try {
1091+
JpegImage.requestMemoryAllocation(bytesNeeded);
10411092
var image = {
10421093
width: decoder.width,
10431094
height: decoder.height,

test/fixtures/black-6000x6000.jpg

390 KB
Loading

test/index.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ function fixture(name) {
77
return fs.readFileSync(path.join(__dirname, 'fixtures', name));
88
}
99

10+
const SUPER_LARGE_JPEG_BASE64 =
11+
'/9j/wJ39sP//DlKWvX+7xPlXkJa9f7v8DoDVAAD//zb6QAEAI2cBv3P/r4ADpX8Jf14AAAAAgCPE+VeQlr1/uwCAAAAVALNOjAGP2lIS';
12+
13+
const SUPER_LARGE_JPEG_BUFFER = Buffer.from(SUPER_LARGE_JPEG_BASE64, 'base64');
14+
1015
it('should be able to decode a JPEG', function () {
1116
var jpegData = fixture('grumpycat.jpg');
1217
var rawImageData = jpeg.decode(jpegData);
@@ -216,3 +221,27 @@ it('should be able to encode/decode image with exif data', function () {
216221
var loopImageData = jpeg.decode(new Uint8Array(encodedData.data));
217222
expect(loopImageData.exifBuffer).toEqual(imageData.exifBuffer);
218223
});
224+
225+
it('should be able to decode large images within memory limits', () => {
226+
var jpegData = fixture('black-6000x6000.jpg');
227+
var rawImageData = jpeg.decode(jpegData);
228+
expect(rawImageData.width).toEqual(6000);
229+
expect(rawImageData.height).toEqual(6000);
230+
}, 30000);
231+
232+
// See https://github.com/eugeneware/jpeg-js/issues/53
233+
it('should limit resolution exposure', function () {
234+
expect(() => jpeg.decode(SUPER_LARGE_JPEG_BUFFER)).toThrow(
235+
'maxResolutionInMP limit exceeded by 141MP',
236+
);
237+
});
238+
239+
it('should limit memory exposure', function () {
240+
expect(() => jpeg.decode(SUPER_LARGE_JPEG_BUFFER, {maxResolutionInMP: 500})).toThrow(
241+
/maxMemoryUsageInMB limit exceeded by at least \d+MB/,
242+
);
243+
244+
// Make sure the limit resets each decode.
245+
var jpegData = fixture('grumpycat.jpg');
246+
expect(() => jpeg.decode(jpegData)).not.toThrow();
247+
}, 30000);

0 commit comments

Comments
 (0)