mirror of
https://github.com/morluto/rea.git
synced 2026-11-12 17:59:08 +00:00
fix(audit): charge map reads to the count before opening the member
An oversized or unreadable .map returned before readSourceMap counted it, so the allowance of 32 never ran out. An unexpanded .NET or PyInstaller package could then decompress every listed map member, about 1 MiB each, up to the 4,096 entry cap. The count bounds reads, so examineMap now reserves the slot before it opens the member. A map past the count is skipped unopened, and a linker map past it is still settled from its bounded prefix. An inline data: map is already in memory and keeps its charge at the point it is parsed.
This commit is contained in:
3 files changed
+97
-2
No files matched your search
@@ -54,7 +54,7 @@ A member of a package that inventory did not expand is reported as the host file
|
||||
|
||||
A package member skipped by a read allowance still counts as readable for `packaging.extractable_payload`, because the format stores it unencrypted. An encrypted member or one that failed to read counts as unreadable.
|
||||
|
||||
A `.map` too large to parse is a linker map when its header cannot begin a JSON object. A larger `.map` that begins as JSON, or that has a JavaScript name such as `app.js.map`, is counted as skipped. `native.debug_companions` identifies a companion from its name or from a `.map` body that is not a source map, so its `exposed` result has `inferred` confidence.
|
||||
A map read counts against the source-map allowance when it starts, so an oversized or unreadable `.map` uses a read as a parsed one does. A `.map` too large to parse is a linker map when its header cannot begin a JSON object. A larger `.map` that begins as JSON, or that has a JavaScript name such as `app.js.map`, is counted as skipped. `native.debug_companions` identifies a companion from its name or from a `.map` body that is not a source map, so its `exposed` result has `inferred` confidence.
|
||||
|
||||
## Status
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
MAX_RELEASE_EXPOSURE_MEMBER_PROBE_BYTES,
|
||||
MAX_RELEASE_EXPOSURE_PACKAGED_PE_BYTES,
|
||||
MAX_RELEASE_EXPOSURE_SOURCE_MAP_BYTES,
|
||||
MAX_RELEASE_EXPOSURE_SOURCE_MAPS,
|
||||
MAX_RELEASE_EXPOSURE_TEXT_MEMBER_BYTES,
|
||||
PACKAGED_NATIVE_IMAGES,
|
||||
type ReleaseExposureScan,
|
||||
@@ -1255,6 +1256,97 @@ describe("ReleaseExposureReader member allowances", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("ReleaseExposureReader source-map count", () => {
|
||||
it.each([
|
||||
[
|
||||
"dotnet-bundle",
|
||||
(bytes: Buffer, paths: readonly string[]): Buffer =>
|
||||
dotnetBundleFixture(
|
||||
paths.map((path) => ({ path, bytes, compressed: true })),
|
||||
).bytes,
|
||||
],
|
||||
[
|
||||
"pyinstaller-carchive",
|
||||
(bytes: Buffer, paths: readonly string[]): Buffer =>
|
||||
cArchiveExecutable({
|
||||
carrier: "pe",
|
||||
layout: "2.1",
|
||||
pythonVersion: 310,
|
||||
members: paths.map((name) => ({
|
||||
name,
|
||||
typeCode: "x",
|
||||
compress: true,
|
||||
payload: bytes,
|
||||
})),
|
||||
}),
|
||||
],
|
||||
])(
|
||||
"charges an oversized %s map to the map count before opening it",
|
||||
async (_format, archive) => {
|
||||
const root = await createTestTempDirectory("rea-release-exposure-over-");
|
||||
const count = MAX_RELEASE_EXPOSURE_SOURCE_MAPS + 8;
|
||||
const oversized = Buffer.alloc(
|
||||
MAX_RELEASE_EXPOSURE_SOURCE_MAP_BYTES + 1,
|
||||
0x20,
|
||||
);
|
||||
const paths = Array.from(
|
||||
{ length: count },
|
||||
(_, index) => `m${String(index)}.js.map`,
|
||||
);
|
||||
const target = join(root, "app.dll");
|
||||
await writeFile(target, archive(oversized, paths));
|
||||
let opened = 0;
|
||||
createdReaders.wrap = (reader, path) => {
|
||||
if (path !== target) return reader;
|
||||
const open = reader.open.bind(reader);
|
||||
reader.open = (entry, signal) => {
|
||||
if (entry.path.endsWith(".js.map")) opened += 1;
|
||||
return open(entry, signal);
|
||||
};
|
||||
return reader;
|
||||
};
|
||||
try {
|
||||
const scanned = await inspect(root);
|
||||
expect(opened).toBe(MAX_RELEASE_EXPOSURE_SOURCE_MAPS);
|
||||
expect(scanned.source_maps).toMatchObject({
|
||||
embedded: 0,
|
||||
skipped: count,
|
||||
});
|
||||
expect(scanned.text_members.skipped).toBe(count);
|
||||
} finally {
|
||||
createdReaders.wrap = undefined;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it("keeps a linker map's outcome past the count once oversized maps use it", async () => {
|
||||
const root = await createTestTempDirectory("rea-release-exposure-spent-");
|
||||
const oversized = Buffer.alloc(
|
||||
MAX_RELEASE_EXPOSURE_SOURCE_MAP_BYTES + 1,
|
||||
0x20,
|
||||
);
|
||||
oversized.write("Archive member included to satisfy reference\n");
|
||||
await writeFile(
|
||||
join(root, "app.dll"),
|
||||
dotnetBundleFixture(
|
||||
Array.from(
|
||||
{ length: MAX_RELEASE_EXPOSURE_SOURCE_MAPS + 1 },
|
||||
(_, index) => ({
|
||||
path: `m${String(index)}.map`,
|
||||
bytes: oversized,
|
||||
compressed: true,
|
||||
}),
|
||||
),
|
||||
).bytes,
|
||||
);
|
||||
const scanned = await inspect(root);
|
||||
expect(scanned.debug_companions.paths).toHaveLength(
|
||||
MAX_RELEASE_EXPOSURE_SOURCE_MAPS + 1,
|
||||
);
|
||||
expect(scanned.source_maps.skipped).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
const bundled = async (
|
||||
files: readonly { path: string; bytes: Buffer }[],
|
||||
): Promise<string> => {
|
||||
|
||||
@@ -925,6 +925,9 @@ const examineMap = async (
|
||||
state.skippedMaps += 1;
|
||||
return null;
|
||||
}
|
||||
// The read is what the count bounds, so it is charged before the member is
|
||||
// opened. An oversized or unreadable map spends its slot like a parsed one.
|
||||
if (hasSlot) state.mapsRead += 1;
|
||||
const loaded = await map.read(
|
||||
hasSlot
|
||||
? MAX_RELEASE_EXPOSURE_SOURCE_MAP_BYTES
|
||||
@@ -1008,7 +1011,6 @@ const readSourceMap = (
|
||||
map: Pick<MemberView, "logical" | "location">,
|
||||
bytes: Buffer,
|
||||
): void => {
|
||||
state.mapsRead += 1;
|
||||
try {
|
||||
const inspected = inspectSourceMap(bytes.toString("utf8"));
|
||||
const embedded = inspected.declarations.some(
|
||||
@@ -1173,6 +1175,7 @@ const examineInlineMaps = (
|
||||
state.skippedMaps += 1;
|
||||
continue;
|
||||
}
|
||||
state.mapsRead += 1;
|
||||
// An inline declaration is a JavaScript map even if its JSON is invalid.
|
||||
readSourceMap(
|
||||
state,
|
||||
|
||||
Reference in new issue
Block a user