Possible out-of-bounds read in JPEG encoding from unchecked width/height vs buffer size
I found a possible out-of-bounds read (heap over-read with information disclosure) in
JpegEncoder::encode. The Jpeg constructor (Jpeg::New) accepts a JS Buffer, a width,
and a height as independent arguments, validating only that the first is a Buffer and that
width/height are non-negative integers. It never checks that the buffer contains
width * height * channels bytes. The encoder then reads height scanlines of
width * channels bytes directly out of the buffer's backing store. A caller passing a small
buffer with large width/height causes reads far past the end of the buffer, and the over-read
bytes are compressed into the returned JPEG and handed back to JS — leaking adjacent heap memory.
File: src/jpeg_encoder.cpp (missing validation in src/jpeg.cpp, Jpeg::New)
Function: JpegEncoder::encode
// src/jpeg.cpp — Jpeg::New: the ONLY input checks, then raw pointer captured
if (!Buffer::HasInstance(args[0])) return VException("First argument must be Buffer.");
if (!args[1]->IsInt32()) return VException("Second argument must be integer width.");
if (!args[2]->IsInt32()) return VException("Third argument must be integer height.");
int w = args[1]->Int32Value();
int h = args[2]->Int32Value();
if (w < 0) return VException("Width can't be negative.");
if (h < 0) return VException("Height can't be negative.");
...
Local<Object> buffer = args[0]->ToObject();
Jpeg *jpeg = new Jpeg((unsigned char*) Buffer::Data(buffer), w, h, buf_type); // no length check
// src/jpeg_encoder.cpp — JpegEncoder::encode
switch (buf_type) {
case BUF_RGBA: rgb_data = rgba_to_rgb(data, width*height*4); ... break; // reads w*h*4
case BUF_BGRA: rgb_data = bgra_to_rgb(data, width*height*4); ... break; // reads w*h*4
case BUF_BGR: rgb_data = bgr_to_rgb(data, width*height*3); ... break; // reads w*h*3
case BUF_RGB: rgb_data = data; break;
}
...
while (cinfo.next_scanline < cinfo.image_height) {
row_pointer = &rgb_data[start + cinfo.next_scanline*3*width]; // reads 3*width per scanline
jpeg_write_scanlines(&cinfo, &row_pointer, 1);
}
Jpeg::New captures Buffer::Data(buffer) with caller-supplied w/h, checking only that
they are >= 0. No check ties the buffer's byte length to width * height * channels.
- For RGBA/BGRA/BGR, the
*_to_rgb helpers are explicitly told to read width*height*4 or
width*height*3 bytes from data. For RGB, encode reads 3*width bytes per scanline for
height scanlines = width*height*3 bytes.
- When the JS buffer is smaller, these reads run out of bounds.
- The over-read bytes are compressed via libjpeg into
jpeg, copied into a new Buffer, and
returned to the JS callback (JpegEncodeSync and the async UV_JpegEncode), disclosing
adjacent heap memory.
JS trigger (if applicable):
const jpeg = require('./build/Release/jpeg').Jpeg;
const j = new jpeg(Buffer.alloc(4), 1000, 1000, 'rgb'); // claims 1000x1000, buffer is 4 bytes
const leaked = j.encodeSync(); // returned JPEG contains adjacent heap memory
Suggested fix: in Jpeg::New, reject the call with a VException when
Buffer::Length(buffer) < channels(buf_type) * width * height (computed with overflow checks),
before constructing the Jpeg.
Additional defect: use-after-free of the input Buffer (no retention)
Unlike node-png — whose Png::New retains the input Buffer via
png->handle_->SetHiddenValue(String::New("buffer"), args[0]) — Jpeg::New stores only the raw
Buffer::Data(buffer) pointer and never keeps a reference to the input Buffer:
// src/jpeg.cpp — Jpeg::New: no SetHiddenValue / persistent handle on args[0]
Local<Object> buffer = args[0]->ToObject();
Jpeg *jpeg = new Jpeg((unsigned char*) Buffer::Data(buffer), w, h, buf_type); // raw pointer only
jpeg->Wrap(args.This());
return args.This();
JpegEncoder holds that bare pointer as unsigned char *data; (data(ddata) in its constructor).
Because nothing ties the input Buffer's lifetime to the Jpeg object, a caller can construct the
Jpeg, drop the last JS reference to the source Buffer, and let V8 free (or relocate) its backing
store before calling the asynchronous encode(). UV_JpegEncode then dereferences the freed
pointer on the libuv thread pool — a use-after-free (read of freed memory folded into the output
JPEG), in addition to the OOB read above. (The async path does jpeg->Ref(), which pins the Jpeg
wrapper itself, but not the input Buffer.)
Additional fix (UAF): retain the input Buffer for the object's lifetime — mirror node-png's
SetHiddenValue/persistent-handle approach on args[0], or copy the pixel bytes into
Jpeg-owned storage in the constructor.
Possible out-of-bounds read in JPEG encoding from unchecked width/height vs buffer size
I found a possible out-of-bounds read (heap over-read with information disclosure) in
JpegEncoder::encode. TheJpegconstructor (Jpeg::New) accepts a JSBuffer, awidth,and a
heightas independent arguments, validating only that the first is a Buffer and thatwidth/height are non-negative integers. It never checks that the buffer contains
width * height * channelsbytes. The encoder then readsheightscanlines ofwidth * channelsbytes directly out of the buffer's backing store. A caller passing a smallbuffer with large width/height causes reads far past the end of the buffer, and the over-read
bytes are compressed into the returned JPEG and handed back to JS — leaking adjacent heap memory.
File:
src/jpeg_encoder.cpp(missing validation insrc/jpeg.cpp,Jpeg::New)Function:
JpegEncoder::encodeJpeg::NewcapturesBuffer::Data(buffer)with caller-suppliedw/h, checking only thatthey are
>= 0. No check ties the buffer's byte length towidth * height * channels.*_to_rgbhelpers are explicitly told to readwidth*height*4orwidth*height*3bytes fromdata. For RGB,encodereads3*widthbytes per scanline forheightscanlines =width*height*3bytes.jpeg, copied into a newBuffer, andreturned to the JS callback (
JpegEncodeSyncand the asyncUV_JpegEncode), disclosingadjacent heap memory.
JS trigger (if applicable):
Suggested fix: in
Jpeg::New, reject the call with aVExceptionwhenBuffer::Length(buffer) < channels(buf_type) * width * height(computed with overflow checks),before constructing the
Jpeg.Additional defect: use-after-free of the input Buffer (no retention)
Unlike node-png — whose
Png::Newretains the input Buffer viapng->handle_->SetHiddenValue(String::New("buffer"), args[0])—Jpeg::Newstores only the rawBuffer::Data(buffer)pointer and never keeps a reference to the input Buffer:JpegEncoderholds that bare pointer asunsigned char *data;(data(ddata)in its constructor).Because nothing ties the input Buffer's lifetime to the
Jpegobject, a caller can construct theJpeg, drop the last JS reference to the source Buffer, and let V8 free (or relocate) its backingstore before calling the asynchronous
encode().UV_JpegEncodethen dereferences the freedpointer on the libuv thread pool — a use-after-free (read of freed memory folded into the output
JPEG), in addition to the OOB read above. (The async path does
jpeg->Ref(), which pins theJpegwrapper itself, but not the input Buffer.)
Additional fix (UAF): retain the input Buffer for the object's lifetime — mirror node-png's
SetHiddenValue/persistent-handle approach onargs[0], or copy the pixel bytes intoJpeg-owned storage in the constructor.