fix(audit): keep embedded leaves beside schema-invalid map fields

The reader recovers independent indexed leaves only after a
SourceMapFormatFailure, but inspectSourceMap let the schema's ZodError
through. A sibling with mappings "!", a section with a bad offset or a
root with a bad field skipped recovery, so a valid leaf with
sourcesContent reported javascript.source_map_sources as unknown.

Normalize schema rejection in SourceMapFormat, which owns these parses,
so every structural failure reaches consumers as a format failure. The
message is unchanged, and the codec and web fetcher already map both
cases to the same result.
This commit is contained in:
Matt Van Horn committed 2026-11-02 06:27:25 -08:00
1 parent e136911605
commit da25b3a691
3 files changed
+87 -31

No files matched your search

+56 -27
View File
@@ -933,33 +933,55 @@ describe("ReleaseExposureReader mixed maps and sibling paths", () => {
},
);
it("keeps an embedded leaf beside a leaf with malformed mappings", async () => {
const root = await createTestTempDirectory("rea-release-malformed-leaf-");
const path = join(root, "app.js.map");
await writeFile(
path,
JSON.stringify({
version: 3,
sections: [
{
offset: { line: 0, column: 0 },
map: JSON.parse(embeddedSourceMap().toString("utf8")) as unknown,
},
{
offset: { line: 1, column: 0 },
map: { version: 3, sources: ["b.js"], names: [], mappings: "AA" },
},
],
}),
);
const scanned = await inspect(path);
expect(scanned.source_maps).toMatchObject({
embedded: 1,
embedded_paths: [path],
unparsed: 1,
});
expect(sourceMapSourcesStatus(scanned)).toBe("exposed");
});
it.each([
{
name: "a truncated mapping segment",
root: {},
second: { offset: { line: 1, column: 0 }, map: malformedLeaf("AA") },
},
{
name: "a schema-invalid mappings string",
root: {},
second: { offset: { line: 1, column: 0 }, map: malformedLeaf("!") },
},
{
name: "a schema-invalid section offset",
root: {},
second: { offset: { line: "1" }, map: malformedLeaf("AAAA") },
},
{
name: "a schema-invalid root field",
root: { file: 5 },
second: { offset: { line: 1, column: 0 }, map: malformedLeaf("AAAA") },
},
])(
"keeps an embedded leaf beside $name",
async ({ root: fields, second }) => {
const root = await createTestTempDirectory("rea-release-malformed-leaf-");
const path = join(root, "app.js.map");
await writeFile(
path,
JSON.stringify({
version: 3,
...fields,
sections: [
{
offset: { line: 0, column: 0 },
map: JSON.parse(embeddedSourceMap().toString("utf8")) as unknown,
},
second,
],
}),
);
const scanned = await inspect(path);
expect(scanned.source_maps).toMatchObject({
embedded: 1,
embedded_paths: [path],
unparsed: 1,
});
expect(sourceMapSourcesStatus(scanned)).toBe("exposed");
},
);
it("does not expand sections past the decoder depth budget", async () => {
const root = await createTestTempDirectory("rea-release-deep-map-");
@@ -2575,6 +2597,13 @@ describe("ReleaseExposureReader member identity", () => {
});
});
const malformedLeaf = (mappings: string) => ({
version: 3,
sources: ["b.js"],
names: [],
mappings,
});
const sourceMapSourcesStatus = (scan: ReleaseExposureScan) =>
assembleReleaseExposureReport(scan, {
electron_family: "not_electron",
+14 -4
View File
@@ -62,6 +62,16 @@ export class SourceMapFormatFailure extends Error {
}
}
/** Report schema rejection through the one failure type consumers classify. */
const parseStructure = <Schema extends z.ZodType>(
schema: Schema,
value: unknown,
): z.output<Schema> => {
const parsed = schema.safeParse(value);
if (parsed.success) return parsed.data;
throw new SourceMapFormatFailure("format", parsed.error.message);
};
/** Raw declaration identity, separate from flattened engine URL identity. */
export interface SourceMapSourceDeclaration {
readonly sectionPath: readonly number[];
@@ -139,7 +149,7 @@ const addSections = ({
}): void => {
options.check?.();
consumeRecords(budget, rawSections.length);
const sections = z.array(sectionSchema).parse(rawSections);
const sections = parseStructure(z.array(sectionSchema), rawSections);
const embeddedMaps: Extract<
SourceMapSection,
{ readonly kind: "embedded" }
@@ -266,7 +276,7 @@ const inspectLeaf = (
node.value === null ||
Array.isArray(node.value)
)
return leafSchema.parse(node.value);
return parseStructure(leafSchema, node.value);
const rawMap = node.value as Readonly<Record<string, unknown>>;
const rawSources = rawMap.sources;
const rawNames = rawMap.names;
@@ -275,7 +285,7 @@ const inspectLeaf = (
consumeRecords(budget, Array.isArray(rawNames) ? rawNames.length : 0);
consumeRecords(budget, Array.isArray(rawContents) ? rawContents.length : 0);
options.check?.();
const leaf = leafSchema.parse(node.value);
const leaf = parseStructure(leafSchema, node.value);
const layout = inspectMappingLayout(leaf.mappings, options.check);
consumeRecords(budget, layout.segments + layout.lines);
if (
@@ -309,7 +319,7 @@ export const inspectSourceMapValue = (
readonly leaves: readonly SourceMapLeaf[];
} => {
options.check?.();
const root = mapHeaderSchema.parse(value);
const root = parseStructure(mapHeaderSchema, value);
const declarations: SourceMapSourceDeclaration[] = [];
const leaves: SourceMapLeaf[] = [];
const pending: PendingMap[] = [
@@ -35,6 +35,23 @@ it("classifies external indexed sections as unsupported with their exact URL", (
}
});
it.each([
["root field", { ...leaf, version: 2 }],
["leaf field", { ...leaf, mappings: "!" }],
[
"section field",
{ version: 3, sections: [{ offset: { line: "0" }, map: leaf }] },
],
])("reports a %s schema rejection as a format failure", (_, map) => {
try {
inspectSourceMap(JSON.stringify(map));
throw new Error("Expected schema rejection");
} catch (error: unknown) {
expect(error).toBeInstanceOf(SourceMapFormatFailure);
expect(error).toMatchObject({ reason: "format" });
}
});
it("uses the same conservative section classification for URL-plus-map and malformed URLs", () => {
const externalUrl = "https://app.test/maps/part.map";
const external = () =>