GHSA-8j4c-6x6g-rq3j
music-metadata: uncatchable process crash parsing a crafted `.dsf` (residual of GHSA-v6c2-xwv6-8xf7)
Packages
| Ecosystem | Package | Affected | Fixed in |
|---|---|---|---|
| npm | music-metadata | < 11.15.0 | 11.15.0 |
Details
As written in the advisory (Markdown source, shown as plain text).
## 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
References
External links from the advisory. They lead to third-party sites.
- https://github.com/Borewit/music-metadata/security/advisories/GHSA-8j4c-6x6g-rq3j
- https://github.com/Borewit/music-metadata/pull/2700
- https://github.com/Borewit/music-metadata/commit/e7fc27a96e789d41ece41fdac590fc7618274a41
- https://github.com/Borewit/music-metadata
- https://github.com/Borewit/music-metadata/releases/tag/v11.15.0