From 8421e3aa5f5f10ed2ca7d57c015d9c5e75ffbac1 Mon Sep 17 00:00:00 2001 From: Lovell Fuller Date: Tue, 20 Jan 2015 14:18:05 +0000 Subject: [PATCH] Add limitInputPixels option to reject input #115 --- README.md | 6 +++++ index.js | 28 +++++++++++++++++--- src/resize.cc | 10 ++++++++ test/unit/io.js | 68 +++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 108 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 2f727161..c44ac16d 100755 --- a/README.md +++ b/README.md @@ -254,6 +254,12 @@ A Promises/A+ promise is returned when `callback` is not provided. An advanced setting that switches the libvips access method to `VIPS_ACCESS_SEQUENTIAL`. This will reduce memory usage and can improve performance on some systems. +#### limitInputPixels(pixels) + +Do not process input images where the number of pixels (width * height) exceeds this limit. + +`pixels` is the integral Number of pixels, with a value between 1 and the default 268402689 (0x3FFF * 0x3FFF). + ### Image transformation options #### resize(width, [height]) diff --git a/index.js b/index.js index b3f238c3..428c0660 100755 --- a/index.js +++ b/index.js @@ -11,6 +11,12 @@ var BluebirdPromise = require('bluebird'); var sharp = require('./build/Release/sharp'); var libvipsVersion = sharp.libvipsVersion(); +var maximum = { + width: 0x3FFF, + height: 0x3FFF, + pixels: Math.pow(0x3FFF, 2) +}; + var Sharp = function(input) { if (!(this instanceof Sharp)) { return new Sharp(input); @@ -21,6 +27,7 @@ var Sharp = function(input) { bufferIn: null, streamIn: false, sequentialRead: false, + limitInputPixels: maximum.pixels, // ICC profiles iccProfilePath: path.join(__dirname, 'icc') + path.sep, // resize options @@ -366,24 +373,37 @@ Sharp.prototype.resize = function(width, height) { if (!width) { this.options.width = -1; } else { - if (typeof width === 'number' && !Number.isNaN(width) && width % 1 === 0 && width > 0 && width <= 0x3FFF) { + if (typeof width === 'number' && !Number.isNaN(width) && width % 1 === 0 && width > 0 && width <= maximum.width) { this.options.width = width; } else { - throw new Error('Invalid width ' + width); + throw new Error('Invalid width (1 to ' + maximum.width + ') ' + width); } } if (!height) { this.options.height = -1; } else { - if (typeof height === 'number' && !Number.isNaN(height) && height % 1 === 0 && height > 0 && height <= 0x3FFF) { + if (typeof height === 'number' && !Number.isNaN(height) && height % 1 === 0 && height > 0 && height <= maximum.height) { this.options.height = height; } else { - throw new Error('Invalid height ' + height); + throw new Error('Invalid height (1 to ' + maximum.height + ') ' + height); } } return this; }; +/* + Limit the total number of pixels for input images + Assumes the image dimensions contained in the file header can be trusted +*/ +Sharp.prototype.limitInputPixels = function(limit) { + if (typeof limit === 'number' && !Number.isNaN(limit) && limit % 1 === 0 && limit > 0 && limit <= maximum.pixels) { + this.options.limitInputPixels = limit; + } else { + throw new Error('Invalid pixel limit (1 to ' + maximum.pixels + ') ' + limit); + } + return this; +}; + /* Write output image data to a file */ diff --git a/src/resize.cc b/src/resize.cc index a0ccdd0c..72e29f69 100755 --- a/src/resize.cc +++ b/src/resize.cc @@ -54,6 +54,7 @@ struct ResizeBaton { void* bufferIn; size_t bufferInLength; std::string iccProfilePath; + int limitInputPixels; std::string output; std::string outputFormat; void* bufferOut; @@ -94,6 +95,7 @@ struct ResizeBaton { ResizeBaton(): bufferInLength(0), + limitInputPixels(0), outputFormat(""), bufferOutLength(0), topOffsetPre(-1), @@ -178,6 +180,12 @@ class ResizeWorker : public NanAsyncWorker { } vips_object_local(hook, image); + // Limit input images to a given number of pixels, where pixels = width * height + if (image->Xsize * image->Ysize > baton->limitInputPixels) { + (baton->err).append("Input image exceeds pixel limit"); + return Error(baton, hook); + } + // Calculate angle of rotation Angle rotation; bool flip; @@ -923,6 +931,8 @@ NAN_METHOD(resize) { } // ICC profile to use when input CMYK image has no embedded profile baton->iccProfilePath = *String::Utf8Value(options->Get(NanNew("iccProfilePath"))->ToString()); + // Limit input images to a given number of pixels, where pixels = width * height + baton->limitInputPixels = options->Get(NanNew("limitInputPixels"))->Int32Value(); // Extract image options baton->topOffsetPre = options->Get(NanNew("topOffsetPre"))->Int32Value(); baton->leftOffsetPre = options->Get(NanNew("leftOffsetPre"))->Int32Value(); diff --git a/test/unit/io.js b/test/unit/io.js index 994bb184..cea6d507 100755 --- a/test/unit/io.js +++ b/test/unit/io.js @@ -524,4 +524,72 @@ describe('Input/output', function() { }); } + describe('Limit pixel count of input image', function() { + + it('Invalid fails - negative', function(done) { + var isValid = false; + try { + sharp().limitInputPixels(-1); + isValid = true; + } catch (e) {} + assert(!isValid); + done(); + }); + + it('Invalid fails - float', function(done) { + var isValid = false; + try { + sharp().limitInputPixels(12.3); + isValid = true; + } catch (e) {} + assert(!isValid); + done(); + }); + + it('Invalid fails - huge', function(done) { + var isValid = false; + try { + sharp().limitInputPixels(Math.pow(0x3FFF, 2) + 1); + isValid = true; + } catch (e) {} + assert(!isValid); + done(); + }); + + it('Invalid fails - string', function(done) { + var isValid = false; + try { + sharp().limitInputPixels('fail'); + isValid = true; + } catch (e) {} + assert(!isValid); + done(); + }); + + it('Same size as input works', function(done) { + sharp(fixtures.inputJpg).metadata(function(err, metadata) { + if (err) throw err; + sharp(fixtures.inputJpg) + .limitInputPixels(metadata.width * metadata.height) + .toBuffer(function(err) { + assert.strictEqual(true, !err); + done(); + }); + }); + }); + + it('Smaller than input fails', function(done) { + sharp(fixtures.inputJpg).metadata(function(err, metadata) { + if (err) throw err; + sharp(fixtures.inputJpg) + .limitInputPixels(metadata.width * metadata.height - 1) + .toBuffer(function(err) { + assert.strictEqual(true, !!err); + done(); + }); + }); + }); + + }); + });