diff --git a/src/domain/javascript/javascriptStaticAnalysisCalls.ts b/src/domain/javascript/javascriptStaticAnalysisCalls.ts index e7c8ee1827..5137e1ebd7 100644 --- a/src/domain/javascript/javascriptStaticAnalysisCalls.ts +++ b/src/domain/javascript/javascriptStaticAnalysisCalls.ts @@ -306,13 +306,32 @@ export const addSourceMapDirectives = ( end === null ) continue; - const match = /^(?:\/\/|\/\*)[#@]\s*sourceMappingURL\s*=\s*([^\s*]+)/u.exec( - source.slice(start, end), - ); - if (match === null) continue; - const declared = match[1]; - if (declared === undefined || declared.length === 0) continue; - const location = rangeForOffsets(source, start, start + match[0].length); + const commentSource = source.slice(start, end); + if ( + !(commentSource.startsWith("//") || commentSource.startsWith("/*")) || + !(commentSource[2] === "#" || commentSource[2] === "@") + ) + continue; + // Single-class searches avoid regex backtracking over unbounded whitespace + // or inline URLs, which can exhaust the engine stack on two-byte strings. + const nameOffset = commentSource.slice(3).search(/\S/u); + if (nameOffset === -1) continue; + const nameStart = 3 + nameOffset; + if (!commentSource.startsWith("sourceMappingURL", nameStart)) continue; + const nameEnd = nameStart + "sourceMappingURL".length; + const equalsOffset = commentSource.slice(nameEnd).search(/\S/u); + if (equalsOffset === -1) continue; + const equalsStart = nameEnd + equalsOffset; + if (!commentSource.startsWith("=", equalsStart)) continue; + const urlOffset = commentSource.slice(equalsStart + 1).search(/\S/u); + if (urlOffset === -1) continue; + const declaredStart = equalsStart + 1 + urlOffset; + const terminator = commentSource.slice(declaredStart).search(/[\s*]/u); + const declaredEnd = + terminator === -1 ? commentSource.length : declaredStart + terminator; + if (declaredEnd === declaredStart) continue; + const declared = commentSource.slice(declaredStart, declaredEnd); + const location = rangeForOffsets(source, start, start + declaredEnd); addFindingOnce(accumulator, `source-map\0${declared}`, () => accumulator.sourceMaps.push({ declared_url: declared, location }), ); diff --git a/src/domain/javascript/javascriptStaticAnalysisSourceMaps.test.ts b/src/domain/javascript/javascriptStaticAnalysisSourceMaps.test.ts index 93a9ddec90..f5664f73c4 100644 --- a/src/domain/javascript/javascriptStaticAnalysisSourceMaps.test.ts +++ b/src/domain/javascript/javascriptStaticAnalysisSourceMaps.test.ts @@ -30,6 +30,60 @@ describe("source map directives", () => { ).toEqual(["first.map", "second.map"]); }); + it("reads a large inline map from a two-byte source without overflowing", () => { + const declared = + "data:application/json;charset=utf-8;base64," + "A".repeat(9_000_000); + const source = `const label = "ğ";\n//# sourceMappingURL=${declared}`; + + expect(sourceMapUrls(source)).toEqual([declared]); + }); + + it.each([ + ["after the marker", "//#", "sourceMappingURL=x"], + ["before the equals sign", "//# sourceMappingURL", "=x"], + ["before the URL", "//# sourceMappingURL=", "x"], + ])("reads 9.5M spaces %s without overflowing", (_label, prefix, suffix) => { + const source = `ğ${prefix}${" ".repeat(9_500_000)}${suffix}`; + expect(analyzeJavaScriptStaticSource(source).source_map_urls).toEqual([ + { + declared_url: "x", + location: { + start: { line: 1, column: 1 }, + end: { line: 1, column: source.length }, + }, + }, + ]); + }); + + it.each([ + "//#", + "//# \t", + "//# sourceMappingURL", + "//# sourceMappingURL \t", + "//# sourceMappingURL=", + "//# sourceMappingURL= \t", + "/*# sourceMappingURL= */", + "//# sourceMappingURLx=app.js.map", + "//# sourceMappingURL app.js.map", + ])("ignores an incomplete or malformed directive: %s", (source) => { + expect(sourceMapUrls(source)).toEqual([]); + }); + + it("preserves Unicode whitespace and URL terminator boundaries", () => { + const source = + "ğ/*@\u00a0sourceMappingURL\u2003=\ufeffapp.js.map* ignored */"; + const declaredEnd = source.indexOf("* ignored"); + expect(analyzeJavaScriptStaticSource(source).source_map_urls).toEqual([ + { + declared_url: "app.js.map", + location: { + start: { line: 1, column: 1 }, + end: { line: 1, column: declaredEnd }, + }, + }, + ]); + }); + it("reports nothing when no directive is present", () => { expect(sourceMapUrls("const value = 1;")).toEqual([]); });