fix(artifacts): classify ZIP offsets beyond EOF as malformed

Reject negative bounded reads before allocation while retaining short-read and exact-EOF behavior. Exercise the corrupted central-directory offset through the public CLI.

Refs #4006, #3953

Co-authored-by: jackie_cqz <88996311+jackie-cqz@users.noreply.github.com>
This commit is contained in:
morluto
2026-10-16 03:38:20 +08:00
parent bfacfbac30
commit c209dcb209
2 changed files with 38 additions and 0 deletions
+7
View File
@@ -127,6 +127,13 @@ class NodeFileReader extends Reader<string> {
const handle = this.#handle;
if (handle === undefined) throw new Error("ZIP reader is not initialized");
const readBytes = Math.min(length, this.size - index);
// A central-directory offset can point past EOF. Type that malformed ZIP
// read before a negative size reaches Buffer.alloc and becomes an I/O error.
if (readBytes < 0)
throw new ArtifactReaderFailure(
"format",
`ZIP archive is malformed: read at ${index} extends beyond the ${this.size}-byte archive`,
);
if (
this.maximumReadBytes !== undefined &&
readBytes > this.maximumReadBytes
@@ -13,6 +13,37 @@ import { parseEvidence } from "../../src/domain/evidence.js";
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
import { cliTest } from "../support/cli/cliFixture.js";
cliTest(
"classifies a ZIP local-header offset beyond EOF as malformed input",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-zip-beyond-eof-");
const archivePath = join(root, "beyond-eof.zip");
const writer = new ZipWriter(new Uint8ArrayWriter());
await writer.add("member.bin", new TextReader("valid member bytes"));
const bytes = Buffer.from(await writer.close());
const central = bytes.indexOf(Buffer.from([0x50, 0x4b, 0x01, 0x02]));
if (central < 0) throw new Error("Expected a ZIP central directory");
bytes.writeUInt32LE(bytes.length + 1024, central + 42);
await writeFile(archivePath, bytes);
const result = await cli.run({
arguments: ["inspect-artifact", archivePath, "--json"],
});
expect(result.exitCode, result.stdout + result.stderr).toBe(1);
expect(result.json).toMatchObject({
code: "artifact_operation_failed",
category: "execution_failure",
message:
"Artifact is invalid or has changed. Get a fresh copy and try again.",
details: {
operation: "inspect_artifact",
reason: "format",
detail: expect.stringContaining("extends beyond"),
},
});
},
);
cliTest(
"inspects and extracts ZIP members with repeated current-directory prefixes",
async ({ cli }) => {