From bde9979b99a23678a25b7f31e99904aede82f1df Mon Sep 17 00:00:00 2001 From: Yarchik Date: Wed, 2 Sep 2026 14:44:24 +0100 Subject: [PATCH] fix: reject a decoded array length that exceeds the stream An array in count mode reads its length from a preceding field and then loops that many times, pushing a decoded element each iteration, with no check that the count fits the stream. A crafted buffer that declares a huge count (e.g. a uint32 length followed by little payload) forces the loop and the result array to grow far past the input, exhausting memory from a handful of bytes. restructure is the decode engine under fontkit, so a schema with a count-prefixed array of a NUL-terminated string (the default String) is a reachable, idiomatic surface. Each element consumes at least one byte, so a count larger than the bytes remaining in the stream is unsatisfiable; reject it before looping. The EOF/lengthInBytes-driven modes are unchanged. --- src/Array.js | 5 +++++ test/Array.js | 9 ++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/Array.js b/src/Array.js index 150c991..939d2e6 100644 --- a/src/Array.js +++ b/src/Array.js @@ -46,6 +46,11 @@ class ArrayT extends Base { } } else { + // A count larger than the remaining bytes cannot be decoded and would allocate unbounded. + if (length > stream.length - stream.pos) { + throw new Error('Array length exceeds stream length'); + } + for (let i = 0, end = length; i < end; i++) { res.push(this.type.decode(stream, ctx)); } diff --git a/test/Array.js b/test/Array.js index bdf5fac..70e649b 100644 --- a/test/Array.js +++ b/test/Array.js @@ -1,5 +1,5 @@ import assert from 'assert'; -import {Array as ArrayT, Pointer, uint8, uint16, DecodeStream, EncodeStream} from 'restructure'; +import {Array as ArrayT, String as StringT, Pointer, uint8, uint16, uint32, DecodeStream, EncodeStream} from 'restructure'; describe('Array', function() { describe('decode', function() { @@ -62,6 +62,13 @@ describe('Array', function() { const array = new ArrayT(uint8); assert.deepEqual(array.fromBuffer(buffer), [1, 2, 3, 4]); }); + + it('should throw when the decoded length exceeds the stream', function() { + // 32-bit count of 100000 elements, but only one byte of payload follows. + const buffer = new Uint8Array([0x00, 0x01, 0x86, 0xa0, 0x00]); + const array = new ArrayT(new StringT(), uint32); + assert.throws(() => array.fromBuffer(buffer), /exceeds stream length/); + }); }); describe('size', function() {