Memory-exhaustion DoS in BMP decoder (bundled bmp-ts dependency) via unbounded width/height
- Dominant language
- TypeScript
- Stars
- 14.7k
- Forks
- 777
- PR merge metrics
- No merged PRs in 30d
Description
## Summary
`@jimp/js-bmp` delegates BMP decoding entirely to the `bmp-ts` npm package with no validation of its own. `bmp-ts`'s decoder allocates its output pixel buffer directly from the BMP header's `width`/`height` fields with no upper bound and no comparison against the actual input size:
```js
// bmp-ts@1.0.9 dist/commonjs/decoder.js:153
this.data = Buffer.alloc(this.width * this.height * 4);
```
For RLE4/RLE8-compressed BMPs, the very next thing that happens is:
```js
// decoder.js:201 (bit4) and :279 (bit8)
this.data.fill(0);
```
`Buffer.fill()` forces real physical memory to be committed across the whole buffer (unlike `Buffer.alloc`'s lazily-zero-mapped pages), and this happens **before any actual pixel bytes are read from the file**, so a tiny file with a big declared width/height is enough to force a large real memory allocation.
## Proof of Concept
A crafted 1078-byte BMP (valid `BM` magic + 40-byte BITMAPINFOHEADER, `bitPP=8`, `compression=1` i.e. BI_RLE8, `width=10000, height=10000`, a 256-entry zero palette, and **no actual RLE pixel data** after the palette) causes roughly 405MB of RSS growth in a single `Jimp.read()` call, confirmed against the real published `jimp@1.6.1` (which bundles `bmp-ts@1.0.9`):
```
input size 1078 bytes rss before ~67.7MB
Jimp.read(buffer) -> eventually throws RangeError (only AFTER the .fill(0) commits memory)
time 278ms rss after ~472.4MB (~405MB forced commit from a 1KB file)
```
Scaling `width`/`height` further is unbounded — nothing in `bmp-ts` or in `@jimp/js-bmp`'s wrapper (`plugins/js-bmp/src/index.ts`, which calls `BMP.decode()` directly with no width/height sanity check before delegating) caps the allocation. A handful of concurrent uploads of ~1KB crafted files could force multi-GB memory growth or an allocator OOM in any service that calls `Jimp.read()` on user-uploaded images (a very common pattern — avatar/image upload handling).
This is a memory-exhaustion availability issue (CWE-400/789), not memory corruption — Node's `Buffer`/TypedArray bounds checks prevent any actual out-of-bounds read/write.
## Suggested Fix
Before allocating the output buffer, validate that `width * height * bytesPerPixel` is consistent with the actual input buffer size (or with a reasonable configurable maximum), and reject the file early if not. This applies both in `bmp-ts`'s `decoder.js` (root cause) and as a defense-in-depth check in `@jimp/js-bmp`'s wrapper before it delegates to `bmp-ts`, since jimp is the package most people actually depend on directly.
## Disclosure note
Checked for existing reports first: no GHSA/CVE/OSV advisory exists for `bmp-ts`, `jimp`, or `@jimp/js-bmp`. Neither repo has SECURITY.md or GitHub private vulnerability reporting enabled, so filing this as a plain public issue rather than a private advisory — severity is DoS-only (not RCE/memory-corruption), so this seemed reasonable to disclose directly; happy to move to a private channel if one gets set up.
Contributor guide
Research direction
Start with plugins/js-bmp/src/index.ts and the bundled bmp-ts decoder.js allocation and fill paths described in the report. Run the provided crafted BMP reproduction against Jimp.read() to establish the memory-growth behavior, then add coverage for rejecting oversized dimensions before allocation. Done means the reproduction is rejected without the large allocation and normal BMP decoding remains functional.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- node.js, typescript
- Domain
- security
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 62/100