fix(browser): reject overlapping indexed source maps

Fixes #6562
This commit is contained in:
morluto
2026-10-23 18:00:47 +08:00
parent 553262443a
commit ba7c3dafab
8 changed files with 151 additions and 23 deletions
+5 -6
View File
@@ -1,8 +1,5 @@
import * as t from "@babel/types";
import {
decodeValidatedSourceMapLeaves,
isBeforeSourceMapLeafStop,
} from "../javascript/sourceMaps/DecodedSourceMap.js";
import { decodeValidatedSourceMapLeaves } from "../javascript/sourceMaps/DecodedSourceMap.js";
import { sanitizeBrowserUrl } from "../domain/browserObservation.js";
import { AnalysisResourceConstraintError } from "../domain/analysisErrorCore.js";
@@ -653,7 +650,6 @@ const normalizeSourceMap = (
for (const [line, row] of rows.entries()) {
if ((line & 255) === 0) check();
for (const segment of row) {
if (!isBeforeSourceMapLeafStop(leaf, line, segment[0])) continue;
if (segment.length === 1) continue;
const rawSource = leaf.map.sources[segment[1]];
if (rawSource === undefined) continue;
@@ -741,7 +737,10 @@ const normalizeSourceMap = (
return emptySourceMapItem(
request,
"invalid",
"Source-map JSON mappings could not be decoded safely.",
cause instanceof SourceMapFormatFailure &&
cause.reason === "section-overlap"
? cause.message
: "Source-map JSON mappings could not be decoded safely.",
fetchedUrl,
);
}
@@ -5,7 +5,7 @@ import {
} from "./SourceMapFormat.js";
/** Determine whether a leaf-local mapping falls before its enclosing boundary. */
export const isBeforeSourceMapLeafStop = (
const isBeforeSourceMapLeafStop = (
leaf: SourceMapLeaf,
line: number,
column: number,
@@ -40,6 +40,11 @@ export const decodeValidatedSourceMapLeaves = (
const generatedLine = leaf.offset.line + line;
const generatedColumn =
segment[0] + (line === 0 ? leaf.offset.column : 0);
if (!isBeforeSourceMapLeafStop(leaf, line, segment[0]))
throw new SourceMapFormatFailure(
"section-overlap",
"Embedded source-map mappings overlap the following section boundary.",
);
if (
!Number.isSafeInteger(generatedLine) ||
!Number.isSafeInteger(generatedColumn) ||
@@ -50,7 +50,11 @@ const execute = async (): Promise<string> => {
throw cause;
return JSON.stringify({
state: "failure",
reason: cause instanceof SourceMapFormatFailure ? cause.reason : "format",
reason:
cause instanceof SourceMapFormatFailure &&
cause.reason !== "section-overlap"
? cause.reason
: "format",
message: cause.message,
});
}
+1 -1
View File
@@ -55,7 +55,7 @@ export interface SourceMapStructureOptions {
/** A decoder-profile failure with an actionable producer constraint. */
export class SourceMapFormatFailure extends Error {
constructor(
readonly reason: "format" | "unsupported" | "limit",
readonly reason: "format" | "unsupported" | "limit" | "section-overlap",
message: string,
) {
super(message);
@@ -310,7 +310,7 @@ describe("source-map layout admission", () => {
{ offset: { line: 0, column: 5 }, map: leaf },
],
}),
).toThrow(/overlap/u);
).toThrow(expect.objectContaining({ reason: "section-overlap" }));
});
it("accepts absent optional names and null contents without changing reported identity", () => {
expect(
+1 -12
View File
@@ -22,10 +22,7 @@ import {
SourceMapFormatFailure,
type SourceMapSourceDeclaration,
} from "./SourceMapFormat.js";
import {
decodeValidatedSourceMapLeaves,
isBeforeSourceMapLeafStop,
} from "./DecodedSourceMap.js";
import { decodeValidatedSourceMapLeaves } from "./DecodedSourceMap.js";
/** Decode with the pinned upstream codec; preserve every equal-position candidate. */
export const traceSourceMap = (
@@ -35,14 +32,6 @@ export const traceSourceMap = (
): WebSourceMapReport => {
const inspected = inspectSourceMap(text);
const decodedLeaves = decodeValidatedSourceMapLeaves(inspected.leaves, url);
for (const { leaf, rows } of decodedLeaves)
for (const [line, row] of rows.entries())
for (const segment of row)
if (!isBeforeSourceMapLeafStop(leaf, line, segment[0]))
throw new SourceMapFormatFailure(
"format",
"Embedded source-map mappings overlap the following section boundary.",
);
let map: TraceMap;
if (inspected.format === "regular") {
const decoded = decodedLeaves[0];
@@ -127,6 +127,7 @@ const cases = [
[1, 10, "b.ts"],
],
dedicatedControl: false,
expectedStatus: "invalid",
},
{
name: "nested gap clips previous parent leaf",
@@ -177,6 +178,7 @@ const cases = [
[1, 8, "b.ts"],
],
dedicatedControl: false,
expectedStatus: "invalid",
},
{
name: "empty indexed sibling clips previous leaf",
@@ -210,6 +212,7 @@ const cases = [
},
expected: [[1, 0, "a.ts"]],
dedicatedControl: false,
expectedStatus: "invalid",
},
{
name: "depth-three same-line composition",
@@ -307,12 +310,16 @@ const cases = [
[4, 1, "b.ts"],
],
dedicatedControl: false,
expectedStatus: "invalid",
},
] as const;
it.each(cases)(
"preserves indexed coordinate contract: $name",
async ({ map, expected, dedicatedControl }) => {
async (testCase) => {
const { map, expected, dedicatedControl } = testCase;
const expectedStatus =
"expectedStatus" in testCase ? testCase.expectedStatus : "included";
const origin = "https://example.test";
const url = origin + "/app.js.map";
const text = JSON.stringify(map);
@@ -340,6 +347,15 @@ it.each(cases)(
{ fetch: () => Promise.resolve(new Response(text, { status: 200 })) },
);
const item = result.items[0];
if (expectedStatus === "invalid") {
expect(item).toMatchObject({
status: "invalid",
artifact: null,
mappings: [],
limitation: expect.stringContaining("overlap"),
});
return;
}
if (item?.status !== "included") throw new Error(JSON.stringify(result));
expect(
item.mappings.map((entry) => [
@@ -358,3 +374,76 @@ it.each(cases)(
]);
},
);
it("rejects a source mapping that crosses an indexed section boundary", async () => {
const origin = "https://example.test";
const url = `${origin}/app.js.map`;
const makeMap = (firstMappings: string) => ({
version: 3,
sections: [
{
offset: { line: 0, column: 0 },
map: {
version: 3,
sources: ["a.js"],
sourcesContent: ["x();"],
names: [],
mappings: firstMappings,
},
},
{
offset: { line: 0, column: 1 },
map: {
version: 3,
sources: ["b.js"],
sourcesContent: ["y();"],
names: [],
mappings: "AAAA",
},
},
],
});
const input = analyzeWebBundleInputSchema.parse({
cdp_endpoint: "http://127.0.0.1:9222",
allowed_origins: [origin],
target_id: "page-1",
fetch_source_maps: true,
});
const request = {
scriptKey: "scr_" + "1".repeat(64),
declaredUrl: url,
fetchUrl: url,
};
const fetchMap = (map: ReturnType<typeof makeMap>) =>
fetchWebSourceMaps([request], input, undefined, {
fetch: () =>
Promise.resolve(new Response(JSON.stringify(map), { status: 200 })),
});
const overlapping = makeMap("AAAA,EAAA");
const overlapText = JSON.stringify(overlapping);
expect(() =>
traceSourceMap(overlapText, url, { line: 1, column: 2 }),
).toThrow(/overlap/u);
const overlappingResult = await fetchMap(overlapping);
expect(overlappingResult).toMatchObject({
status: "unavailable",
requested: 1,
processed: 1,
});
expect(overlappingResult.items[0]).toMatchObject({
status: "invalid",
artifact: null,
mappings: [],
limitation: expect.stringContaining("overlap"),
});
const controlResult = await fetchMap(makeMap("AAAA"));
expect(controlResult.items[0]).toMatchObject({
status: "included",
limitation: null,
mappings: [
{ generated_line: 1, generated_column: 0, source: `${origin}/a.js` },
{ generated_line: 1, generated_column: 1, source: `${origin}/b.js` },
],
});
});
@@ -1,6 +1,7 @@
import { fileURLToPath } from "node:url";
import { access } from "node:fs/promises";
import { expect, it, vi } from "vitest";
import { AnalysisInputError } from "../../../src/domain/analysisErrorCore.js";
import { SourceMapDecoder } from "../../../src/javascript/sourceMaps/SourceMapDecoder.js";
import {
SOURCE_MAP_CODEC_MEMORY_LIMIT_BYTES,
@@ -139,6 +140,47 @@ it.each([
if (response.ok) throw new Error("expected failed map");
expect(response.error._tag).toBe(tag);
});
it("keeps indexed section overlap typed as a malformed source map", async () => {
const text = JSON.stringify({
version: 3,
sections: [
{
offset: { line: 0, column: 0 },
map: {
version: 3,
sources: ["a.js"],
sourcesContent: ["x();"],
names: [],
mappings: "AAAA,EAAA",
},
},
{
offset: { line: 0, column: 1 },
map: {
version: 3,
sources: ["b.js"],
sourcesContent: ["y();"],
names: [],
mappings: "AAAA",
},
},
],
});
const response = await realDecoder().trace({ ...input, text });
if (response.ok) throw new Error("expected overlapping map rejection");
expect(response.error).toBeInstanceOf(AnalysisInputError);
if (!(response.error instanceof AnalysisInputError))
throw new Error("expected malformed source-map input error");
expect(response.error.issues).toEqual(
expect.arrayContaining([
expect.objectContaining({
reason: "invalid_format",
message: expect.stringContaining("overlap"),
}),
]),
);
});
it("reports cleanup failure with the actual owning operation", async () => {
const response = await realDecoder({
createRuntime: async () => {