fix(browser): validate and bound source-map evidence

This commit is contained in:
morluto
2026-10-09 06:17:10 +08:00
parent e1df5e8555
commit 0a0174b9ac
11 changed files with 918 additions and 178 deletions
@@ -0,0 +1,82 @@
import { TraceMap, decodedMappings } from "@jridgewell/trace-mapping";
import {
SourceMapFormatFailure,
type SourceMapLeaf,
} from "./SourceMapFormat.js";
/** Determine whether a leaf-local mapping falls before its enclosing boundary. */
export const isBeforeSourceMapLeafStop = (
leaf: SourceMapLeaf,
line: number,
column: number,
): boolean => {
if (leaf.stop === null) return true;
const generatedLine = leaf.offset.line + line;
const generatedColumn = column + (line === 0 ? leaf.offset.column : 0);
return (
generatedLine < leaf.stop.line ||
(generatedLine === leaf.stop.line && generatedColumn < leaf.stop.column)
);
};
/** Resolve one declaration with the same pinned upstream implementation used by full maps. */
export const resolveSourceMapSource = (
source: string | null,
sourceRoot: string | null | undefined,
mapUrl: string,
): string => {
const map = new TraceMap(
{
version: 3,
names: [],
sources: [source ?? ""],
...(sourceRoot == null ? {} : { sourceRoot }),
mappings: "",
},
mapUrl,
);
const resolved = map.resolvedSources[0];
if (resolved === undefined)
throw new SourceMapFormatFailure(
"format",
"Source-map resolver did not return the requested source identity.",
);
return resolved;
};
/** Decode leaf mappings and validate producer indices before any consumer publishes them. */
export const decodeValidatedSourceMapLeaves = (
leaves: readonly SourceMapLeaf[],
mapUrl: string,
check?: () => void,
): readonly {
readonly leaf: SourceMapLeaf;
readonly rows: ReturnType<typeof decodedMappings>;
readonly resolvedSources: readonly string[];
}[] =>
leaves.map((leaf) => {
check?.();
const map = new TraceMap(JSON.stringify(leaf.map), mapUrl);
const rows = decodedMappings(map);
for (const [line, row] of rows.entries()) {
if ((line & 255) === 0) check?.();
for (const segment of row) {
const generatedLine = leaf.offset.line + line;
const generatedColumn =
segment[0] + (line === 0 ? leaf.offset.column : 0);
if (
!Number.isSafeInteger(generatedLine) ||
!Number.isSafeInteger(generatedColumn) ||
![1, 4, 5].includes(segment.length) ||
segment.some((value) => !Number.isSafeInteger(value) || value < 0) ||
(segment.length !== 1 && segment[1] >= leaf.map.sources.length) ||
(segment.length === 5 && segment[4] >= leaf.map.names.length)
)
throw new SourceMapFormatFailure(
"format",
"Decoded local or absolute position, source index or name index is invalid.",
);
}
}
return { leaf, rows, resolvedSources: map.resolvedSources };
});
+5 -14
View File
@@ -311,18 +311,10 @@ export const inspectSourceMap = (
};
};
/** Validate shared source-map structure before a consumer decodes mappings. */
export const validateSourceMapStructure = (
/** Return validated leaves when a consumer needs decoded rows after preflight. */
export const inspectSourceMapValue = (
value: unknown,
options: SourceMapStructureOptions = {},
): void => {
inspectSourceMapValue(value, options, false);
};
const inspectSourceMapValue = (
value: unknown,
options: SourceMapStructureOptions = {},
collect = true,
): {
readonly format: "regular" | "indexed";
readonly reportedFile: string | null;
@@ -351,10 +343,9 @@ const inspectSourceMapValue = (
continue;
}
const leaf = inspectLeaf(node, budget, options);
if (collect)
leaves.push({ map: leaf, offset: node.offset, stop: node.stop });
for (const [sourceIndex, source] of leaf.sources.entries())
if (collect)
leaves.push({ map: leaf, offset: node.offset, stop: node.stop });
if (options.profile !== "browser-collection")
for (const [sourceIndex, source] of leaf.sources.entries())
declarations.push({
sectionPath: node.path,
sourceIndex,
+23 -42
View File
@@ -20,6 +20,10 @@ import {
SourceMapFormatFailure,
type SourceMapSourceDeclaration,
} from "./SourceMapFormat.js";
import {
decodeValidatedSourceMapLeaves,
isBeforeSourceMapLeafStop,
} from "./DecodedSourceMap.js";
/** Decode with the pinned upstream codec; preserve every equal-position candidate. */
export const traceSourceMap = (
@@ -28,7 +32,21 @@ export const traceSourceMap = (
position: WebSourcePosition,
): WebSourceMapReport => {
const inspected = inspectSourceMap(text);
const { map, rows } = decodeValidatedMap(inspected, url);
const map = new AnyMap(inspected.jsonText, url);
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.",
);
const rows =
inspected.format === "regular"
? (decodedLeaves[0]?.rows ?? [])
: decodedMappings(map);
validateFlattenedRows(inspected, rows, map);
const matched = greatestPosition(rows, position);
const segments =
matched === null
@@ -77,49 +95,14 @@ export const traceSourceMap = (
});
};
const decodeValidatedMap = (
const validateFlattenedRows = (
inspected: ReturnType<typeof inspectSourceMap>,
url: string,
) => {
let regularRows: ReturnType<typeof decodedMappings> | undefined;
for (const leaf of inspected.leaves) {
const decoded = decodedMappings(
new TraceMap(JSON.stringify(leaf.map), url),
);
if (inspected.format === "regular") regularRows = decoded;
for (const [line, row] of decoded.entries())
for (const segment of row) {
if (
segment.some((value) => !Number.isSafeInteger(value) || value < 0) ||
(segment.length !== 1 && segment[1] >= leaf.map.sources.length) ||
(segment.length === 5 && segment[4] >= leaf.map.names.length)
)
throw new SourceMapFormatFailure(
"format",
"Decoded section-local position, source index or name index is invalid.",
);
const generatedLine = leaf.offset.line + line;
const generatedColumn =
segment[0] + (line === 0 ? leaf.offset.column : 0);
if (
leaf.stop !== null &&
(generatedLine > leaf.stop.line ||
(generatedLine === leaf.stop.line &&
generatedColumn >= leaf.stop.column))
)
throw new SourceMapFormatFailure(
"format",
"Embedded source-map mappings overlap the following section boundary.",
);
}
}
const map = new AnyMap(inspected.jsonText, url);
const rows = regularRows ?? decodedMappings(map);
rows: ReturnType<typeof decodedMappings>,
map: ReturnType<typeof AnyMap>,
): void => {
for (const row of rows)
for (const segment of row) {
if (
![1, 4, 5].includes(segment.length) ||
segment.some((v) => !Number.isSafeInteger(v) || v < 0) ||
(segment.length !== 1 && segment[1] >= inspected.declarations.length) ||
(segment.length === 5 && segment[4] >= map.names.length)
)
@@ -128,8 +111,6 @@ const decodeValidatedMap = (
"Decoded mapping has invalid component count, position, source index or name index.",
);
}
return { map, rows };
};
const greatestPosition = (