# REPRO-2026-00137: ExifReader: unbounded memory amplification DoS via crafted ICC mluc tag ## Summary Status: published Severity: high Type: security Confidence: Unknown ## Identifiers REPRO ID: REPRO-2026-00137 CVE: CVE-2026-8813 ## Package Name: exifreader Ecosystem: npm Affected: < 4.39.0 Fixed: 4.39.0 ## Root Cause # 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. ## Reproduction Details Reproduced: 2026-05-22T09:46:27.064Z Duration: 2145 seconds Tool calls: 158 Turns: 138 Handoffs: 3 ## Quick Verification Run one of these commands to verify locally: pruva-verify REPRO-2026-00137 pruva-verify CVE-2026-8813 Or open in GitHub Codespaces (zero-friction, auto-runs): https://github.com/codespaces/new?ref=repro/REPRO-2026-00137&repo=N3mes1s/pruva-sandbox Or download and run the script manually: curl -O https://api.pruva.dev/v1/reproductions/REPRO-2026-00137/artifacts/repro/reproduction_steps.sh chmod +x reproduction_steps.sh ./reproduction_steps.sh WARNING: Run in a sandboxed environment. This exploits a real vulnerability. ## References - NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-8813 ## Artifacts - repro/rca_report.md (analysis, 5708 bytes) - repro/reproduction_steps.sh (reproduction_script, 6795 bytes) - vuln_variant/rca_report.md (analysis, 8022 bytes) - vuln_variant/reproduction_steps.sh (reproduction_script, 7955 bytes) - bundle/context.json (other, 4421 bytes) - bundle/metadata.json (other, 654 bytes) - bundle/ticket.md (ticket, 5042 bytes) - repro/validation_verdict.json (other, 1384 bytes) - vuln_variant/root_cause_equivalence.json (other, 1158 bytes) - vuln_variant/patch_analysis.md (documentation, 5239 bytes) - vuln_variant/variant_manifest.json (other, 2821 bytes) - vuln_variant/tmp/images/png_classic.png (other, 151 bytes) - vuln_variant/tmp/images/webp_classic.webp (other, 224 bytes) - vuln_variant/tmp/images/jpeg_classic.jpg (other, 220 bytes) - vuln_variant/tmp/test_parse_tags2.js (other, 787 bytes) - vuln_variant/tmp/test_png_debug.js (other, 636 bytes) - vuln_variant/tmp/test_decompress.js (other, 549 bytes) - vuln_variant/tmp/test_png_async.js (other, 538 bytes) - vuln_variant/tmp/test_parse_tags.js (other, 928 bytes) - vuln_variant/tmp/test_png_async2.js (other, 641 bytes) - vuln_variant/tmp/vuln/package.json (other, 56 bytes) - vuln_variant/tmp/test_variant.js (other, 1038 bytes) - vuln_variant/tmp/test_parse_icc.js (other, 769 bytes) - vuln_variant/tmp/fix/package.json (other, 56 bytes) - vuln_variant/tmp/create_images.js (other, 4214 bytes) - vuln_variant/tmp/test_decompression_stream.js (other, 795 bytes) - vuln_variant/validation_verdict.json (other, 2048 bytes) - logs/npm_install_fix.log (log, 0 bytes) - logs/npm_install_vuln.log (log, 0 bytes) - logs/variant_webp_classic_vuln.txt (other, 1005 bytes) - logs/fix_out.txt (other, 40 bytes) - logs/variant_png_classic_fix.txt (other, 72 bytes) - logs/npm_install_vuln_variant.log (log, 0 bytes) - logs/variant_jpeg_classic_fix.txt (other, 73 bytes) - logs/variant_jpeg_classic_vuln.txt (other, 1005 bytes) - logs/variant_png_classic_vuln.txt (other, 73 bytes) - logs/vuln_out.txt (other, 1008 bytes) - logs/npm_install_fix_variant.log (log, 0 bytes) - logs/variant_webp_classic_fix.txt (other, 73 bytes) ## API Access - JSON: https://api.pruva.dev/v1/reproductions/REPRO-2026-00137 - Script: https://api.pruva.dev/v1/reproductions/REPRO-2026-00137/artifacts/repro/reproduction_steps.sh - Web: https://pruva.dev/r/REPRO-2026-00137 ## For AI Assistants To help users with this reproduction: 1. The `pruva-verify` command is the simplest way to run the reproduction 2. The script downloads and runs automatically in an isolated temp directory 3. Always recommend running in a VM, container, or disposable environment 4. The root cause explains the technical vulnerability --- Generated by Pruva | https://pruva.dev