diff --git a/src/decode/decode.test.ts b/src/decode/decode.test.ts index 11b4325..fe765f7 100644 --- a/src/decode/decode.test.ts +++ b/src/decode/decode.test.ts @@ -603,6 +603,46 @@ describe("decode", () => { ); }); + it("throws for out-of-bounds GENERATED_RANGE_SUBRANGE_BINDING variable indices in strict mode", () => { + const encoder = new ItemEncoder(); + encoder.addUnsignedVLQs(Tag.GENERATED_RANGE_START, 0, 0).finishItem(); + encoder.addUnsignedVLQs(Tag.GENERATED_RANGE_SUBRANGE_BINDING, 1_000_000) + .finishItem(); + encoder.addUnsignedVLQs(Tag.GENERATED_RANGE_END, 0).finishItem(); + const map = createMap({ ranges: encoder.encode() }); + + assertThrows( + () => decode(map, { mode: DecodeMode.STRICT }), + Error, + "GENERATED_RANGE_SUBRANGE_BINDING variable index out of bounds", + ); + }); + + it("ignores out-of-bounds GENERATED_RANGE_SUBRANGE_BINDING variable indices in lax mode", () => { + const encoder = new ItemEncoder(); + encoder.addUnsignedVLQs(Tag.GENERATED_RANGE_START, 0, 0).finishItem(); + encoder.addUnsignedVLQs(Tag.GENERATED_RANGE_SUBRANGE_BINDING, 1_000_000) + .finishItem(); + encoder.addUnsignedVLQs(Tag.GENERATED_RANGE_END, 0).finishItem(); + const map = createMap({ ranges: encoder.encode() }); + + const info = decode(map, { mode: DecodeMode.LAX }); + + assertExists(info.ranges[0]); + assertEquals(info.ranges[0].values, []); + }); + + it("ignores huge GENERATED_RANGE_SUBRANGE_BINDING variable indices without hanging", () => { + // "H//////B" encodes variable index 2^31 - 1. This used to create a sparse + // `values` array of length 2^31 and iterate every hole. + const map = createMap({ ranges: "EAA,H//////B,FA" }); + + const info = decode(map, { mode: DecodeMode.LAX }); + + assertExists(info.ranges[0]); + assertEquals(info.ranges[0].values, []); + }); + it("ignores multiple GENERATED_RANGE_SUBRANGE_BINDING items for the same variable in lax mode", () => { const scopeEncoder = new ItemEncoder(); // Original scope with 1 variable. diff --git a/src/decode/decode.ts b/src/decode/decode.ts index e0e5d60..c839a33 100644 --- a/src/decode/decode.ts +++ b/src/decode/decode.ts @@ -34,6 +34,7 @@ import { TokenIterator } from "../vlq.ts"; * - Encountering ORIGINAL_SCOPE_VARIABLES items outside a surrounding scope START/END. * - Encountering GENERATED_RANGE_BINDINGS items outside a surrounding range START/END. * - Miss-matches between the number of variables in a scope vs the number of value expressions in the ranges. + * - GENERATED_RANGE_SUBRANGE_BINDING items whose variable index is out of bounds of the range's bindings. * - Out-of-bound indices into the "names" array. */ export const enum DecodeMode { @@ -622,6 +623,16 @@ class Decoder { if (!subRangeBindings) return; for (const [variableIndex, bindings] of subRangeBindings) { + // Sub-range bindings can only refine an existing binding from the + // GENERATED_RANGE_BINDINGS item. Writing to an arbitrary (attacker + // controlled) index would turn `range.values` into a huge sparse array. + if (variableIndex < 0 || variableIndex >= range.values.length) { + this.#throwInStrictMode( + "GENERATED_RANGE_SUBRANGE_BINDING variable index out of bounds", + ); + continue; + } + const value = range.values[variableIndex]; const subRanges: SubRangeBinding[] = []; range.values[variableIndex] = subRanges;