diff --git a/src/artifacts/CpioArtifactReader.ts b/src/artifacts/CpioArtifactReader.ts index 1cb12bae03..31f3181622 100644 --- a/src/artifacts/CpioArtifactReader.ts +++ b/src/artifacts/CpioArtifactReader.ts @@ -335,6 +335,7 @@ export class CpioArtifactReader implements ArtifactReader { "format", "cpio trailer declares member data", ); + await drain(this.#verified(source, header, raw)); await source.finish(); yield* this.#unresolvedLinks(); return; diff --git a/src/artifacts/InstallerPackageLimits.test.ts b/src/artifacts/InstallerPackageLimits.test.ts index 380342661d..34e6994d7e 100644 --- a/src/artifacts/InstallerPackageLimits.test.ts +++ b/src/artifacts/InstallerPackageLimits.test.ts @@ -3,6 +3,7 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { buffer } from "node:stream/consumers"; import { Readable } from "node:stream"; +import { gunzipSync, gzipSync } from "node:zlib"; import { expect, it } from "vitest"; import { ArtifactDecodedBudget } from "./ArtifactDecodedBudget.js"; import { CpioArtifactReader } from "./CpioArtifactReader.js"; @@ -233,3 +234,64 @@ it("does not expose a partial inventory when a previous TOC load failed", async }, ); }); + +it("rejects a TOC checksum when the fixed header declares none", async () => { + const bytes = Buffer.from( + xarArchive([{ name: "a", data: Buffer.from("ok") }]), + ); + bytes.writeUInt32BE(0, 24); + await withReader(bytes, async (reader) => { + await expect( + (async () => { + for await (const entry of reader.entries()) void entry; + })(), + ).rejects.toMatchObject({ + reason: "format", + message: expect.stringContaining("declares no TOC checksum"), + }); + }); +}); + +it.each(["", "nothex", "a".repeat(63)])( + "rejects malformed checksum text %j before applying integrity policy", + async (digest) => { + const bytes = rewriteXarToc( + xarArchive([{ name: "a", data: Buffer.from("ok") }]), + (xml) => + xml.replace( + /[^<]*<\/extracted-checksum>/u, + `${digest}`, + ), + ); + await withReader(bytes, async (reader) => { + await expect( + (async () => { + for await (const entry of reader.entries()) void entry; + })(), + ).rejects.toMatchObject({ + reason: "format", + message: "xar extracted-checksum has malformed sha256 digest text", + }); + }); + }, +); + +it("rejects a CRC trailer whose declared checksum is not empty-data zero", async () => { + const raw = gunzipSync(gzipCpio([], "crc")); + raw.write("00000001", 102, "ascii"); + const reader = new CpioArtifactReader(async () => + Readable.from([gzipSync(raw)]), + ); + try { + await expect( + (async () => { + for await (const entry of reader.entries()) void entry; + })(), + ).rejects.toMatchObject({ + reason: "integrity", + message: "cpio CRC disagrees with content: TRAILER!!!", + }); + } finally { + await reader.close(); + } +}); diff --git a/src/artifacts/XarArtifactReader.ts b/src/artifacts/XarArtifactReader.ts index 4793d61d72..c68248ad23 100644 --- a/src/artifacts/XarArtifactReader.ts +++ b/src/artifacts/XarArtifactReader.ts @@ -101,6 +101,16 @@ const parseChecksum = ( const value = (element.textContent ?? "").trim().toLowerCase(); if (CHECKSUM_ALGORITHMS.get(style) === undefined) return { unsupported: style || "(missing style)", value }; + const expected = DIGEST_BYTES.get(style); + if ( + expected === undefined || + value.length !== expected * 2 || + !/^[0-9a-f]+$/u.test(value) + ) + throw new ArtifactReaderFailure( + "format", + `xar ${tag} has malformed ${style} digest text`, + ); return { algorithm: style, value }; }; @@ -529,6 +539,11 @@ export class XarArtifactReader implements ArtifactReader { `xar header declares unsupported TOC checksum algorithm ${headerAlg}`, ); const checksum = childElement(toc, "checksum"); + if (expectedStyle === null && checksum !== undefined) + throw new ArtifactReaderFailure( + "format", + "xar header declares no TOC checksum but the TOC supplies one", + ); if (checksum === undefined) { if (headerAlg !== 0) throw new ArtifactReaderFailure(