What's the vulnerability?

exifreader is a widely used JavaScript library for extracting metadata (EXIF, IPTC, XMP, ICC, ...) from images. When it parses an image's embedded ICC color profile, it interprets ICC tags including the mluc (multi-localized Unicode) tag type.

The mluc tag layout declares a record count and a record size, both read from attacker-controlled bytes in the profile. Through version 4.38.1 the parser does not validate these fields against the actual length of the profile buffer. A crafted mluc tag that sets:

  • a very large record count, and
  • a record size of zero

makes the parsing loop reprocess the same record over and over and append an output entry on every iteration. Because the record size is zero the cursor never advances and the loop runs count times, appending without bound. The result is unbounded memory growth (memory amplification): a tiny crafted image inflates into hundreds of megabytes / gigabytes of allocations until the Node.js process is OOM-killed.

This is a remote, unauthenticated denial of service: any application that parses an attacker-supplied image with ExifReader (upload endpoints, thumbnailers, metadata extractors) can be taken down by a single crafted file.

Root Cause Analysis

# Root Cause Analysis: CVE-2026-8813

## Summary

ExifReader versions prior to 4.39.0 contain a denial-of-service vulnerability in their ICC profile parser. When parsing a crafted ICC profile containing an `mluc` (multi-localized Unicode) tag with a zero `recordSize` and a very large `numRecords` value, the parser enters an unbounded loop that repeatedly appends entries to an output array without advancing the read cursor. This causes unbounded memory growth (memory amplification), eventually exhausting the JavaScript heap and triggering an out-of-memory (OOM) crash.

## Impact

- **Package/component affected**: `exifreader` npm package (ICC tag parsing module, specifically `src/icc-tags.js`)
- **Affected versions**: `< 4.39.0` (vulnerable), specifically tested on `4.38.1`
- **Fixed versions**: `4.39.0`
- **Risk level and consequences**: High (CVSS 3.1 base 7.5). Any application that parses attacker-supplied images using ExifReader (image upload endpoints, thumbnailers, metadata extractors) can be crashed remotely by a single crafted ~220-byte JPEG file. The impact is a complete denial of service via process OOM.

## Root Cause

The vulnerability exists in `src/icc-tags.js` in the `parseTags` function, specifically in the `TAG_TYPE_MULTI_LOCALIZED_UNICODE_TYPE` (`mluc`) handling branch. The vulnerable code (from v4.38.1) reads two attacker-controlled 32-bit fields from the ICC profile buffer:

1. `numRecords` at `tagOffset + 8`
2. `recordSize` at `tagOffset + 12`

It then enters a `for` loop that iterates `numRecords` times. On each iteration it reads a record from `offset`, pushes a parsed entry object into the `val` array, and then advances `offset += recordSize`. When `recordSize` is zero, `offset` never advances, so the same bytes are re-read on every iteration. Because `numRecords` is also attacker-controlled (e.g., 10,000,000), the loop runs unboundedly, appending millions of objects to the `val` array and causing catastrophic heap growth.

Additionally, even if `recordSize` is non-zero but large, the loop could read beyond the buffer bounds, although the zero-record-size case is the most direct amplification vector.

The fix commit [`c9d88b67e127b2dcc7b46e328df468257fb2dc30`](https://github.com/mattiasw/ExifReader/commit/c9d88b67e127b2dcc7b46e328df468257fb2dc30) adds two validations before entering the `mluc` loop:

1. A minimum record-size check: `recordSize < 12` (the size of a single record) causes an early return.
2. A bounds check: `numRecords * recordSize` must not exceed the remaining buffer length after the `mluc` header (`dataView.byteLength - tagOffset - 16`).

These checks prevent both the zero-record-size infinite loop and the out-of-bounds over-read scenarios.

## Reproduction Steps

The reproduction is fully automated in `repro/reproduction_steps.sh`.

What the script does:
1. Installs `exifreader@4.38.1` (vulnerable) and `exifreader@4.39.0` (fixed) into separate temporary directories.
2. Constructs a minimal 220-byte JPEG containing an APP0 JFIF segment and an APP2 ICC profile segment.
3. The embedded ICC profile is 180 bytes and contains:
   - A valid `acsp` signature.
   - One tag-table entry pointing to an `mluc` tag at offset 144.
   - The `mluc` tag declares `numRecords = 10,000,000` and `recordSize = 0`.
4. Runs a Node.js harness against the malicious image for each version, capping the heap at 128 MB and enforcing a 10-second timeout.
5. Compares exit codes, duration, and memory growth.

Expected evidence:
- **Vulnerable (4.38.1)**: The Node.js process OOM-crashes within ~1.5 seconds with exit code 134 (`FATAL ERROR: Ineffective mark-compacts near heap limit`).
- **Fixed (4.39.0)**: The parse completes successfully in ~3–4 ms with ~1.25 MB RSS growth, returning the parsed ICC header tags and skipping the malformed `mluc` tag.

## Evidence

- Log directory: `logs/`
- `logs/npm_install_vuln.log` / `logs/npm_install_fix.log` — package installation output
- `logs/vuln_out.txt` — OOM stack trace from the vulnerable version:
  ```
  FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory
  ```
- `logs/fix_out.txt` — successful parse output from the fixed version:
  ```
  SUCCESS duration_ms=4 delta_rss_mb=1.25
  ```
- Environment: Node.js v22.22.2 on Linux x64.
- Idempotency: The reproduction script was executed twice consecutively with identical results.

## Recommendations / Next Steps

1. **Upgrade immediately** to `exifreader@4.39.0` or later. The patch is a minimal, targeted bounds-check addition with no breaking API changes.
2. **Input validation** for applications that accept arbitrary user images: consider pre-scanning or sandboxing image parsing operations, and enforce resource limits (CPU time, memory) on metadata-extraction workers.
3. **Regression testing**: Add a unit test that feeds the same 220-byte malicious JPEG to `ExifReader.load()` and asserts that it returns within a short timeout and without excessive memory growth.
4. **Audit related parsers**: Check other tag-type parsers in `src/icc-tags.js` (e.g., `desc`, `text`, `sig`) for similar missing bounds checks.

## Additional Notes

- The crafted image file is only 220 bytes, yet it can crash a Node.js process with a 128 MB heap limit in under 2 seconds. This demonstrates the severity of memory-amplification bugs.
- The reproduction is fully idempotent: `repro/reproduction_steps.sh` cleans up its temporary directories on exit and can be re-run from any working directory.
- The script uses `NODE_PATH` injection to load the correct `exifreader` version without conflicting installations, ensuring clean isolation between the vulnerable and fixed test cases.
One Command

Verify with pruva-verify

Run the Pruva CLI to automatically fetch and execute the reproduction script.

pruva-verify REPRO-2026-00137
or pruva-verify CVE-2026-8813
Install: curl -fsSL https://pruva.dev/install.sh | sh

Or Run Manually

1

Download the script

curl -O https://pruva.dev/api/v1/reproductions/REPRO-2026-00137/artifacts/reproduction_steps.sh
2

Make executable

chmod +x reproduction_steps.sh
3

Run the script

./reproduction_steps.sh
Run in a VM, container, or disposable environment. This exploits a real vulnerability.

How Pruva Reproduced This

Watch the AI agent's step-by-step process.

Loading session...

Artifacts

No artifacts available