GHSA-8j4c-6x6g-rq3j: Medium severity npm/music-metadata vulnerability
Summary DsfParser.parseChunks skips an unrecognised chunk's payload with an un-awaited call: js this.tokenizer.ignore(Number(chunkHeader.size) - ChunkHeader.len); // lib/dsf/DsfParser.js:51 — no await ChunkHeader.len is 12. A crafted .dsf chunk with id != 'fmt ' and size in 0..11 makes the argument negative; strtok3 (≥ 10.3.5) throws RangeError on a negative ignore. Because the call is fire-and-forget, the rejection is detached from the parseBuffer() promise chain → unhandled rejection → Node's default (≥ 15) crashes the process — after parseBuffer() already resolved, so a caller's try/catch catches nothing and is still taken down.
Residual of GHSA-v6c2-xwv6-8xf7: the ASF site was fixed in 11.12.3 (size validation) and strtok3 now throws on negative ignore; the DSF site was never validated, and its missing await escalates that throw into an uncatchable crash.
Root cause (lib/dsf/DsfParser.js:34-56) js while (bytesRemaining >= ChunkHeader.len) { // ChunkHeader.len = 12 const chunkHeader = await this.tokenizer.readToken(ChunkHeader); // { id, size } switch (chunkHeader.id) { case 'fmt ': { ...; return; } default: this.tokenizer.ignore(Number(chunkHeader.size) - ChunkHeader.len); break; // size<12 -> negative, no await } bytesRemaining -= chunkHeader.size; } strtok3 AbstractTokenizer.ignore (L78-79): if (length < 0) throw new RangeError('ignore length must be ≥ 0 bytes');
Steps to reproduce repro/ — public API only, Node's default unhandled-rejection mode, try/catch around the parse: npm install && node poc.mjs Confirmed on 11.14.0: [app] parseBuffer() RESOLVED — the caller saw no error to catch. RangeError: ignore length must be ≥ 0 bytes at DsfParser.parseChunks (.../lib/dsf/DsfParser.js:51) <process exits non-zero — the "process survived" line never prints>
Impact DoS: a single crafted .dsf (or any file with the DSD magic) crashes the Node process of any app parsing untrusted audio with music-metadata (2.2M weekly downloads). The crash bypasses the caller's error handling, so even apps that correctly try/catch per-file parsing are killed — one malicious upload can take down a shared server/worker.
Remediation Add await on line 51 (makes the RangeError a catchable parse error), and validate chunkHeader.size >= ChunkHeader.len before the skip (as the ASF fix did; also guards the loop counter). Audit other parsers for un-awaited tokenizer.ignore()/readToken().
Scope / honesty Requires the DSF path (a DSD -magic file — normal auto-detection). Relies on Node's default unhandled-rejection mode (throw, default since Node 15); the point is that the standard defensive per-parse try/catch does not protect against it. Crash (availability), not disclosure/RCE. Negatives confirmed alongside: ASF infinite loop fixed; negative-ignore infinite-loop class closed at strtok3; unbounded allocation bounded by strtok3's read bound-check.
Credits Issue also reported by @ryu7eroo
Affected Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
npm/music-metadatato a version that resolves this vulnerability.Fixed in 11.15.0 - Compensating control
In DsfParser.parseChunks, await the tokenizer.ignore() call for unrecognised chunks and validate that chunkHeader.size >= ChunkHeader.len before skipping the payload; ChunkHeader.len is 12.
- Compensating control
Audit other parsers for un-awaited tokenizer.ignore() and readToken() calls so parser errors remain attached to the parse promise chain.
Event History
Frequently Asked Questions
Which deployments are exposed to a process crash?
Deployments that parse crafted .dsf files with music-metadata are exposed. The described crash behavior applies to Node.js 15 and later, where an unhandled rejection crashes the process by default.
What does an attacker need to include in a DSF file to trigger the failure?
The file must contain an unrecognised DSF chunk whose size field is between 0 and 11 inclusive. This causes the parser to call ignore with a negative value, which strtok3 version 10.3.5 or later rejects with a RangeError.
Can an application catch this failure by wrapping parseBuffer() in try/catch?
No. The parser invokes the failing ignore call without awaiting it, so parseBuffer() can resolve before the rejection occurs; the rejection is detached from the returned promise chain and is not caught by the caller's try/catch.