Skip to content

fix: reject an array length that exceeds the remaining buffer - #300

Open
spokodev wants to merge 1 commit into
keichi:masterfrom
spokodev:fix/array-length-exceeds-buffer
Open

fix: reject an array length that exceeds the remaining buffer#300
spokodev wants to merge 1 commit into
keichi:masterfrom
spokodev:fix/array-length-exceeds-buffer

Conversation

@spokodev

@spokodev spokodev commented Sep 2, 2026

Copy link
Copy Markdown

An array whose length is read from a parsed field — the common length-prefixed shape, e.g. a uint32 count followed by that many records — generates a loop bounded only by that field:

for (var $c = <length>; $c > 0; $c--) { ... }

The parser definition is developer-written (trusted), but the buffer passed to parse() is attacker-controlled. When the element decodes via buffer.subarray() (a nested Parser, a buffer, or a string), reading past the end of the input does not throw, so a tiny frame that declares a huge count walks offset past EOF while allocating one object per declared element. A uint32 count lets a ≤6-byte buffer drive a multi-GB allocation / OOM.

Repro

const parser = new Parser().uint32be("count").array("records", {
  length: "count",
  type: new Parser().buffer("payload", { length: 2 }),
});
parser.parse(Buffer.from([0xff, 0xff, 0xff, 0xff])); // 4 bytes -> tries 4,294,967,295 elements

Measured (constant 4-byte input, only the declared count varies): count=10M → ~1.1 s / ~1.8 GB heap; count=40M with --max-old-space-size=1024FATAL ERROR: JavaScript heap out of memory. (Primitive-typed arrays are not affected — DataView.getX throws past EOF — so only the subarray-based element shapes are exploitable.)

Fix

Every array element consumes at least one byte, so a count larger than the number of bytes remaining is unsatisfiable. Guard the count against buffer.length - offset before the loop; this rejects only physically-impossible counts and never fires on valid data (the readUntil and lengthInBytes paths are untouched).

Test

Adds a case: a uint32be count of 100000 followed by no element bytes must throw. It fails on master (the parser silently produces 100000 elements) and passes with the fix. Full suite: 168 passing (was 166; the new case runs under both the Buffer and Uint8Array parametrizations).

An array whose `length` is read from a parsed field (e.g. a length-
prefixed list of records) generates a loop bounded only by that field:

    for (var $c = <length>; $c > 0; $c--) { ... }

The count is taken from attacker-controlled input, and elements decoded
via `buffer.subarray()` (a nested Parser, a buffer or a string) do not
throw past the end of the input, so a tiny frame declaring a huge count
walks the offset past EOF while allocating one object per declared
element. A uint32 count lets a <=6-byte buffer force a multi-GB
allocation / OOM.

Guard the count against the bytes remaining before looping: every array
element consumes at least one byte, so a count larger than the remaining
buffer is unsatisfiable and can be rejected without affecting valid
input.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant