pdf-import.sandbox.test.ts

4.6 KB · raw

1// T-007: MV-9 pdf.js sandbox flags regression test2//3// Why this test exists: MV-9 hardened the pdf.js getDocument call with four4// security options (isEvalSupported:false, disableFontFace/Range/Stream:true)5// to defend against CVE-2024-4367-class eval-via-font attacks and limit the6// attack surface of a crafted PDF in the Codex restore path. No test asserts7// these options are present. A refactor that silently removes or inverts any8// flag re-exposes the attack surface with no test failure.910import { beforeEach, describe, expect, it, vi } from "vitest";1112// ---------------------------------------------------------------------------13// Mock pdfjs-dist before importing pdf-import.ts.14// importCodexPDF uses a lazy `import("pdfjs-dist")` that is cached in a15// module-level variable. vi.mock intercepts dynamic imports too, so the mock16// is in place before any test runs.17// ---------------------------------------------------------------------------1819const getDocumentMock = vi.fn();2021vi.mock("pdfjs-dist", () => ({22  GlobalWorkerOptions: { workerSrc: "" },23  getDocument: getDocumentMock,24}));2526import { importCodexPDF } from "./pdf-import";2728// A minimal File object that passes the size check (< MAX_PDF_BYTES = 25 MiB)29// and has a non-zero size. importCodexPDF calls file.arrayBuffer() before30// getDocument, so we need to return a valid ArrayBuffer.31function makeFakeFile(bytes: Uint8Array = new Uint8Array([0x25, 0x50, 0x44, 0x46])): File {32  return {33    size: bytes.length,34    name: "test.pdf",35    arrayBuffer: async () => bytes.buffer,36    type: "application/pdf",37  } as unknown as File;38}3940describe("T-007: MV-9 pdf.js sandbox flags", () => {41  beforeEach(() => {42    vi.clearAllMocks();4344    // Return a valid-looking pdf object that won't crash importCodexPDF.45    // We only care about what getDocument was called WITH, not what it returns.46    getDocumentMock.mockReturnValue({47      promise: Promise.resolve({48        numPages: 0,49        getPage: vi.fn(),50      }),51    });52  });5354  // T-007a: the four hardened options are all present and correct.55  // Bug this catches: any one of these flags being removed or set to the wrong56  // value re-opens the attack surface guarded by MV-9.57  it("T-007a: getDocument called with all four MV-9 hardened options", async () => {58    const file = makeFakeFile();59    await importCodexPDF(file);6061    expect(getDocumentMock).toHaveBeenCalledOnce();62    const callArgs = getDocumentMock.mock.calls[0][0] as Record<string, unknown>;6364    expect(callArgs.isEvalSupported).toBe(false);65    expect(callArgs.disableFontFace).toBe(true);66    expect(callArgs.disableRange).toBe(true);67    expect(callArgs.disableStream).toBe(true);68  });6970  // T-007b: none of the hardened flags are missing.71  // An undefined flag is as dangerous as false/true in the wrong direction —72  // pdf.js uses its own defaults when a flag is absent.73  it("T-007b: hardened flags are explicitly set, not absent", async () => {74    const file = makeFakeFile();75    await importCodexPDF(file);7677    const callArgs = getDocumentMock.mock.calls[0][0] as Record<string, unknown>;7879    expect("isEvalSupported" in callArgs).toBe(true);80    expect("disableFontFace" in callArgs).toBe(true);81    expect("disableRange" in callArgs).toBe(true);82    expect("disableStream" in callArgs).toBe(true);83  });8485  // T-007c: hardened options do not break the import flow — PDF import still86  // succeeds (or fails for PDF-content reasons, not for options reasons).87  // This is a regression guard: the flags must not make the PDF importer crash.88  it("T-007c: hardened options do not prevent getDocument from being called", async () => {89    const file = makeFakeFile();90    // We don't care whether the import succeeds (numPages=0 means no chunks91    // found); we care that getDocument was actually called (not short-circuited92    // due to the hardened options).93    await importCodexPDF(file);9495    expect(getDocumentMock).toHaveBeenCalledOnce();96  });9798  // T-007d: isEvalSupported must be false, not truthy.99  // A refactor that accidentally passes isEvalSupported:true re-enables the100  // eval path that CVE-2024-4367 exploited. The strictness of the check matters.101  it("T-007d: isEvalSupported is strictly false, not just falsy", async () => {102    const file = makeFakeFile();103    await importCodexPDF(file);104105    const callArgs = getDocumentMock.mock.calls[0][0] as Record<string, unknown>;106    expect(callArgs.isEvalSupported).toStrictEqual(false);107    expect(callArgs.isEvalSupported).not.toBeUndefined();108    expect(callArgs.isEvalSupported).not.toBeNull();109    expect(callArgs.isEvalSupported).not.toBe(0);110  });111});