decompression-bomb.test.ts

1.9 KB · raw

1// MV-4: decompression-bomb output bounds on the zstd and pako legs.2import { describe, expect, it } from "vitest";3import { deflate, inflate } from "pako";4import { compressZstd, decompressZstd } from "./zstd";5import { IMPORT_LIMITS } from "../shared/import-limits";6import { inflateBounded } from "../popup/codex/generate";78describe("MV-4 decompressZstd output cap", () => {9  it("throws IMPORT_TOO_LARGE when the decompressed output exceeds the cap", async () => {10    // Zeros compress to a tiny frame but decompress past the 64 MiB ceiling.11    const huge = new Uint8Array(IMPORT_LIMITS.MAX_DECOMPRESSED_BYTES + 1);12    const compressed = await compressZstd(huge);13    expect(compressed.byteLength).toBeLessThan(huge.byteLength);14    await expect(decompressZstd(compressed)).rejects.toThrow(/IMPORT_TOO_LARGE/);15  });1617  it("decompresses a normal payload unchanged (cap does not break legit use)", async () => {18    const payload = new TextEncoder().encode("a normal little vault section");19    const round = await decompressZstd(await compressZstd(payload));20    expect(round).toEqual(payload);21  });22});2324describe("MV-4 inflateBounded", () => {25  it("throws when inflated output exceeds the supplied ceiling (deflate bomb)", () => {26    // 8 MiB of zeros deflates tiny; cap it at 1 MiB to trip the ceiling.27    const bomb = deflate(new Uint8Array(8 * 1024 * 1024));28    expect(bomb.length).toBeLessThan(1024 * 1024);29    expect(() => inflateBounded(bomb, 1024 * 1024)).toThrow(/IMPORT_TOO_LARGE/);30  });3132  it("returns bytes identical to one-shot inflate for an in-bounds payload", () => {33    const original = new TextEncoder().encode(34      JSON.stringify({ entries: Array.from({ length: 200 }, (_, i) => ({ id: i })) }),35    );36    const compressed = deflate(original);37    const bounded = inflateBounded(compressed, IMPORT_LIMITS.MAX_DECOMPRESSED_BYTES);38    expect(bounded).toEqual(inflate(compressed));39    expect(bounded).toEqual(original);40  });41});