mirror of
https://github.com/morluto/rea.git
synced 2026-11-12 17:59:08 +00:00
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:
3 files changed
+87
-31
No files matched your search
@@ -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",
|
||||
|
||||
@@ -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 = () =>
|
||||
|
||||
Reference in new issue
Block a user