Skip to content

Commit

Permalink
fix: Validate chunk types
Browse files Browse the repository at this point in the history
Check chunk types symbol range and whether it's known critical chunk
  • Loading branch information
madtisa committed Aug 12, 2024
1 parent 64ff954 commit 60087bc
Show file tree
Hide file tree
Showing 2 changed files with 91 additions and 16 deletions.
36 changes: 33 additions & 3 deletions src/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
]),
),
)
})
71 changes: 58 additions & 13 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -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--
Expand All @@ -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<number>([
chunkTypes.imageHeader,
chunkTypes.palette,
chunkTypes.imageData,
chunkTypes.imageEnd,
])

export default function isApng(buffer: Uint8Array): boolean {
const minChunkSize = headerSizes.LENGTH + headerSizes.TYPE + headerSizes.CRC

Expand All @@ -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)
}
Expand Down

0 comments on commit 60087bc

Please sign in to comment.