From 8e6f37e2a0873ac21ebcc889c27839bfcb7552ca Mon Sep 17 00:00:00 2001 From: Lucas LSMO Date: Tue, 13 Oct 2026 09:54:37 -0300 Subject: [PATCH] fix(formats): preserve high bits in remaining magic-byte checks (#2384) * fix(formats): preserve high bits in remaining magic-byte checks Node's "ascii" decoder masks the high bit, so byte-for-byte signature comparisons accepted lookalikes such as CD DA ("MZ" | 0x80) or a high-bit "bplist00"/"NIBArchive" header. ac57c88ef fixed this for the shared DOS/MZ reader; apply the same latin1 decoding to the remaining signature checks: NativeAOT and Windows native loader MZ/PE, binary plist and NIBArchive detection, and PNG IHDR checks. Display-name decodes (section and metadata stream names) are unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) * style(windows): format signature admission regressions --------- Co-authored-by: Claude Opus 5.5 (1M context) Co-authored-by: morluto <76467478+morluto@users.noreply.github.com> --- scripts/verify-windows-native-artifact.mjs | 4 ++-- src/domain/native/nativeAotPe.test.ts | 19 +++++++++++++++++++ src/domain/native/nativeAotPe.ts | 4 ++-- src/windows/WindowsNativeLoader.test.ts | 16 ++++++++++++++++ src/windows/WindowsNativeLoader.ts | 4 ++-- 5 files changed, 41 insertions(+), 6 deletions(-) diff --git a/scripts/verify-windows-native-artifact.mjs b/scripts/verify-windows-native-artifact.mjs index 1a6777322..4db66e0ee 100644 --- a/scripts/verify-windows-native-artifact.mjs +++ b/scripts/verify-windows-native-artifact.mjs @@ -27,9 +27,9 @@ if ( ); const offset = bytes.length >= 64 ? bytes.readUInt32LE(0x3c) : bytes.length; if ( - bytes.toString("ascii", 0, 2) !== "MZ" || + bytes.toString("latin1", 0, 2) !== "MZ" || offset > bytes.length - 26 || - bytes.toString("ascii", offset, offset + 4) !== "PE\0\0" || + bytes.toString("latin1", offset, offset + 4) !== "PE\0\0" || bytes.readUInt16LE(offset + 4) !== 0x8664 || bytes.readUInt16LE(offset + 24) !== 0x20b ) diff --git a/src/domain/native/nativeAotPe.test.ts b/src/domain/native/nativeAotPe.test.ts index 8ae624d44..ab031cc85 100644 --- a/src/domain/native/nativeAotPe.test.ts +++ b/src/domain/native/nativeAotPe.test.ts @@ -159,3 +159,22 @@ describe("read-only NativeAOT PE parser", () => { }); }); }); + +describe("NativeAOT PE signature admission", () => { + it.each([ + ["MZ", "Not a PE image"], + ["PE", "Invalid PE signature"], + ])( + "rejects a high-bit lookalike of the %s signature", + (signature, reason) => { + const bytes = nativeAotPeFixture(); + const offset = signature === "MZ" ? 0 : bytes.readUInt32LE(0x3c); + for (const position of [offset, offset + 1]) + bytes.writeUInt8(bytes.readUInt8(position) | 0x80, position); + expect( + parseNativeAotPe(bytes, nativeAotPeDigest(bytes), SUMMARY_BUDGET) + .summary, + ).toMatchObject({ status: "not_applicable", reason }); + }, + ); +}); diff --git a/src/domain/native/nativeAotPe.ts b/src/domain/native/nativeAotPe.ts index b67a58398..7f9c5eb55 100644 --- a/src/domain/native/nativeAotPe.ts +++ b/src/domain/native/nativeAotPe.ts @@ -384,11 +384,11 @@ class PeImage { constructor(bytes: Buffer) { this.#bytes = bytes; - if (bytes.length < 0x40 || bytes.toString("ascii", 0, 2) !== "MZ") + if (bytes.length < 0x40 || bytes.toString("latin1", 0, 2) !== "MZ") throw new Error("Not a PE image"); const peOffset = this.#u32(0x3c); this.#range(peOffset, 24); - if (bytes.toString("ascii", peOffset, peOffset + 4) !== "PE\0\0") + if (bytes.toString("latin1", peOffset, peOffset + 4) !== "PE\0\0") throw new Error("Invalid PE signature"); this.#machine = this.#u16(peOffset + 4); const count = this.#u16(peOffset + 6); diff --git a/src/windows/WindowsNativeLoader.test.ts b/src/windows/WindowsNativeLoader.test.ts index 1aa8d7375..c0fee8273 100644 --- a/src/windows/WindowsNativeLoader.test.ts +++ b/src/windows/WindowsNativeLoader.test.ts @@ -143,6 +143,22 @@ describe("packaged Windows native admission", () => { }, ); + it.each([ + { signature: "MZ", offset: 0 }, + { signature: "PE", offset: 64 }, + ])( + "rejects digest-valid high-bit $signature lookalikes before loading", + ({ offset }) => { + const value = fixture(); + value.bytes[offset] = (value.bytes[offset] ?? 0) | 0x80; + value.manifest.artifactSha256 = createHash("sha256") + .update(value.bytes) + .digest("hex"); + expect(loadWindowsNativeAuthority(value.host).available).toBe(false); + expect(value.loads()).toBe(0); + }, + ); + it("rejects a digest-valid artifact for another machine", () => { const value = fixture(); value.bytes.writeUInt16LE(0xaa64, 68); diff --git a/src/windows/WindowsNativeLoader.ts b/src/windows/WindowsNativeLoader.ts index 7719a0a85..2ab08e1ed 100644 --- a/src/windows/WindowsNativeLoader.ts +++ b/src/windows/WindowsNativeLoader.ts @@ -132,12 +132,12 @@ export const loadWindowsNativeAuthority = ( }; const assertWindowsX64Artifact = (bytes: Buffer): void => { - if (bytes.length < 64 || bytes.toString("ascii", 0, 2) !== "MZ") + if (bytes.length < 64 || bytes.toString("latin1", 0, 2) !== "MZ") throw new Error("Windows native artifact is not a PE image"); const offset = bytes.readUInt32LE(0x3c); if ( offset > bytes.length - 26 || - bytes.toString("ascii", offset, offset + 4) !== "PE\0\0" || + bytes.toString("latin1", offset, offset + 4) !== "PE\0\0" || bytes.readUInt16LE(offset + 4) !== 0x8664 || bytes.readUInt16LE(offset + 24) !== 0x20b )