CVE-2020-8215
Description
A buffer overflow in node-canvas <= 1.6.9 allows denial of service or code execution via crafted images.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
A buffer overflow in node-canvas <= 1.6.9 allows denial of service or code execution via crafted images.
The vulnerability is a buffer overflow in the node-canvas library (versions ≤ 1.6.9) when processing user-supplied images. The root cause lies in the JPEG decoding routines (decodeJPEGIntoSurface, decodeJPEGBufferIntoMimeSurface, and loadJPEGFromBuffer), where the standard jpeg_error_mgr is used without a custom error handler, allowing malformed JPEG data to trigger an unhandled error condition and potentially overflow buffers [1][2].
Exploitation requires an attacker to provide a specially crafted image file to an application using the affected library. No prior authentication or special network position is necessary if the application processes user-uploaded images from untrusted sources. The attack surface is broad because node-canvas is widely used in web applications for server-side image manipulation.
An attacker can cause a denial of service (process crash) or potentially execute arbitrary code. The buffer overflow may corrupt memory in a way that allows an attacker to control program flow [1].
The fix, introduced in commit c3e4ccb1c404da01e83fe5eb3626bf55f7f55957, adds a custom JPEG error manager that uses setjmp/longjmp to safely recover from JPEG errors instead of crashing [2]. Users should upgrade to a patched version (≥ 1.6.10).
AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
canvasnpm | < 1.6.11 | 1.6.11 |
Affected products
2- Range: 0.0.1, 0.0.2, 0.0.3, …
Patches
1c3e4ccb1c404Prevent JPEG errors from crashing process
3 files changed · +90 −5
src/Image.cc+55 −3 modified@@ -19,6 +19,14 @@ typedef struct { } gif_data_t; #endif +#ifdef HAVE_JPEG +#include <setjmp.h> + +struct canvas_jpeg_error_mgr: jpeg_error_mgr { + jmp_buf setjmp_buffer; +}; +#endif + /* * Read closure used by loadFromBuffer. */ @@ -752,6 +760,17 @@ Image::decodeJPEGIntoSurface(jpeg_decompress_struct *args) { return CAIRO_STATUS_SUCCESS; } +/* + * Callback to recover from jpeg errors + */ + +METHODDEF(void) canvas_jpeg_error_exit (j_common_ptr cinfo) { + canvas_jpeg_error_mgr *cjerr = static_cast<canvas_jpeg_error_mgr*>(cinfo->err); + + // Return control to the setjmp point + longjmp(cjerr->setjmp_buffer, 1); +} + #if CAIRO_VERSION_MINOR >= 10 /* @@ -764,8 +783,19 @@ Image::decodeJPEGBufferIntoMimeSurface(uint8_t *buf, unsigned len) { // TODO: remove this duplicate logic // JPEG setup struct jpeg_decompress_struct args; - struct jpeg_error_mgr err; + struct canvas_jpeg_error_mgr err; + args.err = jpeg_std_error(&err); + args.err->error_exit = canvas_jpeg_error_exit; + + // Establish the setjmp return context for canvas_jpeg_error_exit to use + if (setjmp(err.setjmp_buffer)) { + // If we get here, the JPEG code has signaled an error. + // We need to clean up the JPEG object, close the input file, and return. + jpeg_destroy_decompress(&args); + return CAIRO_STATUS_READ_ERROR; + } + jpeg_create_decompress(&args); jpeg_mem_src(&args, buf, len); @@ -858,8 +888,19 @@ Image::loadJPEGFromBuffer(uint8_t *buf, unsigned len) { // TODO: remove this duplicate logic // JPEG setup struct jpeg_decompress_struct args; - struct jpeg_error_mgr err; + struct canvas_jpeg_error_mgr err; + args.err = jpeg_std_error(&err); + args.err->error_exit = canvas_jpeg_error_exit; + + // Establish the setjmp return context for canvas_jpeg_error_exit to use + if (setjmp(err.setjmp_buffer)) { + // If we get here, the JPEG code has signaled an error. + // We need to clean up the JPEG object, close the input file, and return. + jpeg_destroy_decompress(&args); + return CAIRO_STATUS_READ_ERROR; + } + jpeg_create_decompress(&args); jpeg_mem_src(&args, buf, len); @@ -883,8 +924,19 @@ Image::loadJPEG(FILE *stream) { if (data_mode == DATA_IMAGE) { // Can lazily read in the JPEG. // JPEG setup struct jpeg_decompress_struct args; - struct jpeg_error_mgr err; + struct canvas_jpeg_error_mgr err; + args.err = jpeg_std_error(&err); + args.err->error_exit = canvas_jpeg_error_exit; + + // Establish the setjmp return context for canvas_jpeg_error_exit to use + if (setjmp(err.setjmp_buffer)) { + // If we get here, the JPEG code has signaled an error. + // We need to clean up the JPEG object, close the input file, and return. + jpeg_destroy_decompress(&args); + return CAIRO_STATUS_READ_ERROR; + } + jpeg_create_decompress(&args); jpeg_stdio_src(&args, stream);
test/fixtures/chrome.jpg+0 −0 addedtest/image.test.js+35 −2 modified@@ -5,10 +5,12 @@ var Canvas = require('../') , Image = Canvas.Image - , assert = require('assert'); + , assert = require('assert') + , fs = require('fs'); var png_checkers = __dirname + '/fixtures/checkers.png'; var png_clock = __dirname + '/fixtures/clock.png'; +var jpg_chrome = __dirname + '/fixtures/chrome.jpg' describe('Image', function () { it('should require new', function () { @@ -206,4 +208,35 @@ describe('Image', function () { assert.equal(img.src, png_clock + 's3'); assert.equal(onerrorCalled, 0); }); -}); + + it('does not crash on invalid images', function () { + function tryImage (src) { + var img = new Image() + img.src = src + // if we came this far we didn't crash! + } + + function withIncreasedByte (source, index) { + var copy = source.slice(0) + + copy[index] += 1 + + return copy + } + + var source = fs.readFileSync(jpg_chrome) + + tryImage(withIncreasedByte(source, 0)) + tryImage(withIncreasedByte(source, 1)) + tryImage(withIncreasedByte(source, 1060)) + tryImage(withIncreasedByte(source, 1061)) + tryImage(withIncreasedByte(source, 1062)) + tryImage(withIncreasedByte(source, 1063)) + tryImage(withIncreasedByte(source, 1064)) + tryImage(withIncreasedByte(source, 1065)) + tryImage(withIncreasedByte(source, 1066)) + tryImage(withIncreasedByte(source, 1067)) + tryImage(withIncreasedByte(source, 1068)) + tryImage(withIncreasedByte(source, 1069)) + }) +})
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-73rg-x683-m3qwghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2020-8215ghsaADVISORY
- github.com/Automattic/node-canvas/commit/c3e4ccb1c404da01e83fe5eb3626bf55f7f55957ghsaWEB
- hackerone.com/reports/315037ghsax_refsource_MISCWEB
- www.npmjs.com/package/canvasghsaWEB
News mentions
0No linked articles in our index yet.