Root cause
Zero-size entries drive a synchronous parse loop
The attacker-chosen entry count can then drive a long synchronous loop through every parse entry point, blocking the Node.js event loop and growing memory in npm 11.15.0.
Provisional finding. Full causal and release verification remains incomplete.
A sample entry of size zero advanced the parser's cursor by zero bytes, so the attacker-chosen entry count drove a long synchronous loop through every parse entry point. Only npm 11.15.0 shipped the code: 11.14.0 is clean despite the advisory's wider range, and 11.16.0 fixes it.
AI-assisted change
The AI-attributed change made StsdAtom.get advance by size minus the four-byte size field; for a zero-size sample entry that nets no movement.
Root cause
The attacker-chosen entry count can then drive a long synchronous loop through every parse entry point, blocking the Node.js event loop and growing memory in npm 11.15.0.
Fix
The repair limits StsdAtom.get to the atom payload, rejects entries smaller than 16 bytes or larger than the remaining data, and throws on truncation.
Case facts
Cite this finding
Vibe Security Radar, "CVE-2026-107391 in Borewit/music-metadata," Georgia Tech SSLab, https://vibesecradar.com/cves/CVE-2026-107391/
@@ -629,7 +629,8 @@ export class StsdAtom implements IGetToken<IAtomStsd> { const size = Token.UINT32_BE.get(buf, off); // Sample description size off += Token.UINT32_BE.len; table.push(new SampleDescriptionTable(size - Token.UINT32_BE.len).get(buf, off));- off += size;+ // A SampleEntry extends Box, so its size covers the size field already stepped over+ off += size - Token.UINT32_BE.len; } return {@@ -619,14 +619,25 @@ export class StsdAtom implements IGetToken<IAtomStsd> { } public get(buf: Uint8Array, off: number): IAtomStsd {-+ const end = Math.min(off + this.len, buf.length);+ if (end - off < stsdHeader.len) {+ throw new Mp4ContentError('Truncated stsd header');+ } const header = stsdHeader.get(buf, off); off += stsdHeader.len; const table: ISampleDescription[] = []; for (let n = 0; n < header.numberOfEntries; ++n) {+ if (end - off < Token.UINT32_BE.len) {+ throw new Mp4ContentError('Truncated stsd sample entry');+ } const size = Token.UINT32_BE.get(buf, off); // Sample description size+ // A SampleEntry needs its size, format, reserved bytes and data reference index.+ // Requiring it to fit also bounds the loop by the available payload, not entry_count.+ if (size < 16 || size > end - off) {+ throw new Mp4ContentError(`Invalid stsd sample entry size: ${size}`);+ } off += Token.UINT32_BE.len; table.push(new SampleDescriptionTable(size - Token.UINT32_BE.len).get(buf, off)); // A SampleEntry extends Box, so its size covers the size field already stepped overAI-assisted change 31f5e0339d6cf8c63a2d7b51974f359851d047ee169067494472353467447ee3 · Fix 816888abf54bc5096d1951efd2c0044cf2bed1fff5115f50ca6cd3fe20192871