From 60087bc761a385f489fc4b468b043af804a220c8 Mon Sep 17 00:00:00 2001 From: madtisa Date: Mon, 12 Aug 2024 19:49:24 +0300 Subject: [PATCH] fix: Validate chunk types Check chunk types symbol range and whether it's known critical chunk --- src/index.test.js | 36 ++++++++++++++++++++++-- src/index.ts | 71 ++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 91 insertions(+), 16 deletions(-) diff --git a/src/index.test.js b/src/index.test.js index 47f81c9..070d920 100644 --- a/src/index.test.js +++ b/src/index.test.js @@ -120,7 +120,7 @@ test('returns false when acTL is not a chunk type', (t) => { // Chunk length: 4 0x00, 0x00, 0x00, 0x04, // Chunk type: any - 0x00, 0x00, 0x00, 0x01, + 0x66, 0x66, 0x66, 0x66, // Chunk data: acTL 0x61, 0x63, 0x54, 0x4c, // Chunk CRC @@ -151,15 +151,45 @@ test('returns false when next chunk size is too small', (t) => { // Chunk length: 4 0x00, 0x00, 0x00, 0x04, // Chunk type: any - 0x00, 0x00, 0x00, 0x01, + 0x66, 0x66, 0x66, 0x66, // Chunk CRC 0x00, 0x00, 0x00, 0x00, // Chunk length: 4 0x00, 0x00, 0x00, 0x04, // Chunk type: any - 0x00, 0x00, 0x00, 0x02, + 0x66, 0x66, 0x66, 0x66, // Chunk CRC omitted ]), ), ) }) + +test('returns false when chunk type is invalid', (t) => { + t.false( + isApng( + new Uint8Array([ + // PNG header + 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, + // Chunk length: 0 + 0x00, 0x00, 0x00, 0x00, + // Chunk type: invalid bytes + 0x00, 0x00, 0x00, 0x01, + ]), + ), + ) +}) + +test('returns false when unknown critical chunk type', (t) => { + t.false( + isApng( + new Uint8Array([ + // PNG header + 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, + // Chunk length: 0 + 0x00, 0x00, 0x00, 0x00, + // Chunk type: unknown critical + 0x55, 0x55, 0x55, 0x55, + ]), + ), + ) +}) diff --git a/src/index.ts b/src/index.ts index bf59464..d0bf997 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,11 +1,33 @@ -function convertToInt(bytes: Uint8Array) { - return bytes.reduce((value, byte) => (value << 8) + byte) +function bytesToInt(bytes: Uint8Array): number { + return bytes.reduce((value, byte) => (value << 8) + byte, 0) +} + +function chunkTypeToInt(bytes: Uint8Array | number[]): number { + if (bytes.length !== headerSizes.TYPE) { + throw new Error(`Invalid chunk type size ${bytes.length}`) + } + + let value = 0 + for (let i = 0; i < bytes.length; i++) { + const byte = bytes[i] + + if (!(byte >= 0x41 && byte <= 0x5a) && !(byte >= 0x61 && byte <= 0x7a)) { + const bytesText = Array.from(bytes) + .map((byte) => `0x${byte.toString(16)}`) + .join() + throw new Error(`Invalid chunk type ${bytesText}`) + } + + value = (value << 8) + bytes[i] + } + + return value } function isEqual( first: Uint8Array | number[], second: Uint8Array | number[], - length = 4, + length, ) { while (length > 0) { length-- @@ -32,10 +54,25 @@ const headerSizes = { const chunkTypes = { signature: [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a], - animationControl: [0x61, 0x63, 0x54, 0x4c], // 'acTL' - imageData: [0x49, 0x44, 0x41, 0x54], // 'IDAT' + /** `IHDR` chunk type */ + imageHeader: chunkTypeToInt([0x49, 0x48, 0x44, 0x52]), + /** `PLTE` chunk type */ + palette: chunkTypeToInt([0x50, 0x4c, 0x54, 0x45]), + /** `acTL` chunk type */ + animationControl: chunkTypeToInt([0x61, 0x63, 0x54, 0x4c]), + /** `IDAT` chunk type */ + imageData: chunkTypeToInt([0x49, 0x44, 0x41, 0x54]), + /** `IEND` chunk type */ + imageEnd: chunkTypeToInt([0x49, 0x45, 0x4e, 0x44]), } +const knownCriticalChunkTypes = new Set([ + chunkTypes.imageHeader, + chunkTypes.palette, + chunkTypes.imageData, + chunkTypes.imageEnd, +]) + export default function isApng(buffer: Uint8Array): boolean { const minChunkSize = headerSizes.LENGTH + headerSizes.TYPE + headerSizes.CRC @@ -60,21 +97,29 @@ export default function isApng(buffer: Uint8Array): boolean { */ while (buffer.length >= minChunkSize) { - const chunkType = buffer.subarray( - headerSizes.LENGTH, - headerSizes.LENGTH + headerSizes.TYPE, + const chunkType = chunkTypeToInt( + buffer.subarray( + headerSizes.LENGTH, + headerSizes.LENGTH + headerSizes.TYPE, + ), ) - if (isEqual(chunkType, chunkTypes.animationControl, headerSizes.TYPE)) { - return true + // Sixth bit of the first byte of the chunk type is critical property + // (0 - critical, 1 - not) + const isCriticalChunk = !(chunkType & 0x20000000) + if (isCriticalChunk && !knownCriticalChunkTypes.has(chunkType)) { + return false } - if (isEqual(chunkType, chunkTypes.imageData, headerSizes.TYPE)) { - return false + switch (chunkType) { + case chunkTypes.animationControl: + return true + case chunkTypes.imageData: + return false } const nextChunkPosition = - minChunkSize + convertToInt(buffer.subarray(0, headerSizes.LENGTH)) + minChunkSize + bytesToInt(buffer.subarray(0, headerSizes.LENGTH)) buffer = buffer.subarray(nextChunkPosition) }