mirror of
https://github.com/morluto/rea.git
synced 2026-11-12 17:59:08 +00:00
fix(audit): admit binary plists in package member probes
A package member's probe judged text by its own NUL test, which accepted byte order marks but not the binary plist signature decodeMemberText also reads. A PyInstaller executable shipping a binary Localizable.strings was rejected without a scan or a skipped count, so a UTF-16BE private key marker inside it could report not_observed. Name the encodings that explain their own NULs in one predicate, and move the plist fixture builders to module scope.
This commit is contained in:
2 files changed
+56
-24
No files matched your search
@@ -1450,6 +1450,29 @@ const bundled = async (
|
||||
return root;
|
||||
};
|
||||
|
||||
/** One dictionary whose value objects are the given encoded objects. */
|
||||
const binaryPlist = (key: string, value: Buffer): Buffer => {
|
||||
const objects = [
|
||||
Buffer.from([0xd1, 1, 2]),
|
||||
Buffer.concat([Buffer.from([0x50 | key.length]), Buffer.from(key)]),
|
||||
value,
|
||||
];
|
||||
const table =
|
||||
8 + objects.reduce((length, object) => length + object.length, 0);
|
||||
const bytes = Buffer.alloc(table + objects.length + 32);
|
||||
let cursor = 8;
|
||||
const offsets = objects.map((object) => {
|
||||
const offset = cursor;
|
||||
object.copy(bytes, cursor);
|
||||
cursor += object.length;
|
||||
return offset;
|
||||
});
|
||||
return binaryPlistObjectTable(bytes, offsets, table);
|
||||
};
|
||||
/** Length-prefixed object body; every test value here exceeds the short form. */
|
||||
const plistObject = (type: number, length: number, body: Buffer): Buffer =>
|
||||
Buffer.concat([Buffer.from([(type << 4) | 0x0f, 0x10, length]), body]);
|
||||
|
||||
describe("ReleaseExposureReader text encodings", () => {
|
||||
const marker = "-----BEGIN PRIVATE KEY-----\n";
|
||||
const inlineMap = `//# sourceMappingURL=data:application/json;base64,${Buffer.from(
|
||||
@@ -1537,29 +1560,6 @@ describe("ReleaseExposureReader text encodings", () => {
|
||||
expect(members).toEqual({ scanned: 0, skipped: 1 });
|
||||
});
|
||||
|
||||
/** One dictionary whose value objects are the given encoded objects. */
|
||||
const binaryPlist = (key: string, value: Buffer): Buffer => {
|
||||
const objects = [
|
||||
Buffer.from([0xd1, 1, 2]),
|
||||
Buffer.concat([Buffer.from([0x50 | key.length]), Buffer.from(key)]),
|
||||
value,
|
||||
];
|
||||
const table =
|
||||
8 + objects.reduce((length, object) => length + object.length, 0);
|
||||
const bytes = Buffer.alloc(table + objects.length + 32);
|
||||
let cursor = 8;
|
||||
const offsets = objects.map((object) => {
|
||||
const offset = cursor;
|
||||
object.copy(bytes, cursor);
|
||||
cursor += object.length;
|
||||
return offset;
|
||||
});
|
||||
return binaryPlistObjectTable(bytes, offsets, table);
|
||||
};
|
||||
/** Length-prefixed object body; every test value here exceeds the short form. */
|
||||
const plistObject = (type: number, length: number, body: Buffer): Buffer =>
|
||||
Buffer.concat([Buffer.from([(type << 4) | 0x0f, 0x10, length]), body]);
|
||||
|
||||
it.each([
|
||||
["ASCII", plistObject(5, marker.length, Buffer.from(marker))],
|
||||
[
|
||||
@@ -1579,6 +1579,30 @@ describe("ReleaseExposureReader text encodings", () => {
|
||||
expect(members).toEqual({ scanned: 1, skipped: 0 });
|
||||
});
|
||||
|
||||
it("scans a binary plist a package member's name does not announce as text", async () => {
|
||||
const root = await createTestTempDirectory("rea-release-exposure-text-");
|
||||
const wide = plistObject(6, marker.length, utf16(marker, "be").subarray(2));
|
||||
await writeFile(
|
||||
join(root, "app.exe"),
|
||||
cArchiveExecutable({
|
||||
carrier: "pe",
|
||||
layout: "2.1",
|
||||
pythonVersion: 310,
|
||||
members: [
|
||||
{
|
||||
name: "Localizable.strings",
|
||||
typeCode: "x",
|
||||
compress: true,
|
||||
payload: binaryPlist("key", wide),
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
const { texts, members } = await visited(root);
|
||||
expect(texts.join("")).toContain(marker);
|
||||
expect(members).toEqual({ scanned: 1, skipped: 0 });
|
||||
});
|
||||
|
||||
it("keeps an undecodable binary plist's bytes in the scan, unassessed", async () => {
|
||||
const root = await createTestTempDirectory("rea-release-exposure-text-");
|
||||
const bytes = Buffer.concat([
|
||||
|
||||
@@ -1154,7 +1154,7 @@ const examineProbedText = async (
|
||||
if (prefix === null) state.skippedText += 1;
|
||||
else if (
|
||||
prefix.length > 0 &&
|
||||
(!prefix.includes(0) || byteOrderMarkEncoding(prefix) !== null)
|
||||
(!prefix.includes(0) || declaresTextEncoding(prefix))
|
||||
)
|
||||
await examineTextMember(state, member);
|
||||
};
|
||||
@@ -1361,6 +1361,14 @@ const decodeBinaryPlistMember = (bytes: Buffer): MemberText => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Bytes that name their own text encoding, whose NULs are that encoding's
|
||||
* structure rather than a sign of binary data. `decodeMemberText` reads each of
|
||||
* these; a probe that admits on NULs alone would drop them unexamined.
|
||||
*/
|
||||
const declaresTextEncoding = (bytes: Buffer): boolean =>
|
||||
byteOrderMarkEncoding(bytes) !== null || hasBinaryPlistSignature(bytes);
|
||||
|
||||
/**
|
||||
* The text a member's bytes encode, or null when they cannot be read as text.
|
||||
* A member's kind says nothing about its encoding: wide text has no byte order
|
||||
|
||||
Reference in new issue
Block a user