diff --git a/src/decode/decode.test.ts b/src/decode/decode.test.ts index fe765f7..2123391 100644 --- a/src/decode/decode.test.ts +++ b/src/decode/decode.test.ts @@ -770,6 +770,69 @@ describe("decode", () => { assertEquals(info.ranges[0].end, { line: 0, column: 10 }); }); + describe("nested index source maps", () => { + function nestedIndexMap(depth: number): IndexSourceMapJson { + const leaf = encode( + new ScopeInfoBuilder().startRange(0, 0).endRange(0, 10).build(), + ); + let map: IndexSourceMapJson = { + version: 3, + sections: [{ offset: { line: 0, column: 0 }, map: leaf }], + }; + for (let i = 1; i < depth; ++i) { + map = { + version: 3, + // Nested index maps are not allowed by the spec/types. + sections: [{ + offset: { line: 0, column: 0 }, + map: map as unknown as SourceMapJson, + }], + }; + } + return map; + } + + it("skips nested index maps in lax mode", () => { + const map1 = encode( + new ScopeInfoBuilder().startRange(0, 0).endRange(0, 10).build(), + ); + const map: IndexSourceMapJson = { + version: 3, + sections: [ + { offset: { line: 0, column: 0 }, map: map1 }, + { + offset: { line: 1, column: 0 }, + map: nestedIndexMap(2) as unknown as SourceMapJson, + }, + ], + }; + + const info = decode(map, { mode: DecodeMode.LAX }); + + assertEquals(info.ranges.length, 1); + assertEquals(info.ranges[0].start, { line: 0, column: 0 }); + }); + + it("throws for nested index maps in strict mode", () => { + assertThrows( + () => decode(nestedIndexMap(2), { mode: DecodeMode.STRICT }), + Error, + "Index source map sections must not be index maps", + ); + }); + + it("does not overflow the stack for deeply nested index maps", () => { + const map = nestedIndexMap(100_000); + + assertEquals(decode(map, { mode: DecodeMode.LAX }).ranges, []); + assertThrows( + () => decode(map, { mode: DecodeMode.STRICT }), + Error, + "Index source map sections must not be index maps", + ); + }); + }); + it("ignores vendor extensions", () => { const encoder = new ItemEncoder(); encoder.addUnsignedVLQs(Tag.VENDOR_EXTENSION, 0); diff --git a/src/decode/decode.ts b/src/decode/decode.ts index c839a33..64afeb7 100644 --- a/src/decode/decode.ts +++ b/src/decode/decode.ts @@ -36,6 +36,7 @@ import { TokenIterator } from "../vlq.ts"; * - 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. + * - Index source maps with sections that are index source maps themselves. */ export const enum DecodeMode { STRICT = 1, @@ -100,10 +101,20 @@ function decodeIndexMap( }; for (const section of sourceMap.sections) { - const { scopes, ranges, hasVariableAndBindingInfo } = decode(section.map, { - ...options, - generatedOffset: section.offset, - }); + // Per spec, a section's "map" is a regular source map; nested index maps + // are not allowed. Don't recurse, so maliciously deep nesting can't + // overflow the stack. + if ("sections" in section.map) { + if (options.mode === DecodeMode.STRICT) { + throw new Error("Index source map sections must not be index maps"); + } + continue; + } + + const { scopes, ranges, hasVariableAndBindingInfo } = decodeMap( + section.map, + { ...options, generatedOffset: section.offset }, + ); for (const scope of scopes) scopeInfo.scopes.push(scope); for (const range of ranges) scopeInfo.ranges.push(range); scopeInfo.hasVariableAndBindingInfo ||= hasVariableAndBindingInfo;