// T-007: MV-9 pdf.js sandbox flags regression test // // Why this test exists: MV-9 hardened the pdf.js getDocument call with four // security options (isEvalSupported:false, disableFontFace/Range/Stream:true) // to defend against CVE-2024-4367-class eval-via-font attacks and limit the // attack surface of a crafted PDF in the Codex restore path. No test asserts // these options are present. A refactor that silently removes or inverts any // flag re-exposes the attack surface with no test failure. import { beforeEach, describe, expect, it, vi } from "vitest"; // --------------------------------------------------------------------------- // Mock pdfjs-dist before importing pdf-import.ts. // importCodexPDF uses a lazy `import("pdfjs-dist")` that is cached in a // module-level variable. vi.mock intercepts dynamic imports too, so the mock // is in place before any test runs. // --------------------------------------------------------------------------- const getDocumentMock = vi.fn(); vi.mock("pdfjs-dist", () => ({ GlobalWorkerOptions: { workerSrc: "" }, getDocument: getDocumentMock, })); import { importCodexPDF } from "./pdf-import"; // A minimal File object that passes the size check (< MAX_PDF_BYTES = 25 MiB) // and has a non-zero size. importCodexPDF calls file.arrayBuffer() before // getDocument, so we need to return a valid ArrayBuffer. function makeFakeFile(bytes: Uint8Array = new Uint8Array([0x25, 0x50, 0x44, 0x46])): File { return { size: bytes.length, name: "test.pdf", arrayBuffer: async () => bytes.buffer, type: "application/pdf", } as unknown as File; } describe("T-007: MV-9 pdf.js sandbox flags", () => { beforeEach(() => { vi.clearAllMocks(); // Return a valid-looking pdf object that won't crash importCodexPDF. // We only care about what getDocument was called WITH, not what it returns. getDocumentMock.mockReturnValue({ promise: Promise.resolve({ numPages: 0, getPage: vi.fn(), }), }); }); // T-007a: the four hardened options are all present and correct. // Bug this catches: any one of these flags being removed or set to the wrong // value re-opens the attack surface guarded by MV-9. it("T-007a: getDocument called with all four MV-9 hardened options", async () => { const file = makeFakeFile(); await importCodexPDF(file); expect(getDocumentMock).toHaveBeenCalledOnce(); const callArgs = getDocumentMock.mock.calls[0][0] as Record; expect(callArgs.isEvalSupported).toBe(false); expect(callArgs.disableFontFace).toBe(true); expect(callArgs.disableRange).toBe(true); expect(callArgs.disableStream).toBe(true); }); // T-007b: none of the hardened flags are missing. // An undefined flag is as dangerous as false/true in the wrong direction — // pdf.js uses its own defaults when a flag is absent. it("T-007b: hardened flags are explicitly set, not absent", async () => { const file = makeFakeFile(); await importCodexPDF(file); const callArgs = getDocumentMock.mock.calls[0][0] as Record; expect("isEvalSupported" in callArgs).toBe(true); expect("disableFontFace" in callArgs).toBe(true); expect("disableRange" in callArgs).toBe(true); expect("disableStream" in callArgs).toBe(true); }); // T-007c: hardened options do not break the import flow — PDF import still // succeeds (or fails for PDF-content reasons, not for options reasons). // This is a regression guard: the flags must not make the PDF importer crash. it("T-007c: hardened options do not prevent getDocument from being called", async () => { const file = makeFakeFile(); // We don't care whether the import succeeds (numPages=0 means no chunks // found); we care that getDocument was actually called (not short-circuited // due to the hardened options). await importCodexPDF(file); expect(getDocumentMock).toHaveBeenCalledOnce(); }); // T-007d: isEvalSupported must be false, not truthy. // A refactor that accidentally passes isEvalSupported:true re-enables the // eval path that CVE-2024-4367 exploited. The strictness of the check matters. it("T-007d: isEvalSupported is strictly false, not just falsy", async () => { const file = makeFakeFile(); await importCodexPDF(file); const callArgs = getDocumentMock.mock.calls[0][0] as Record; expect(callArgs.isEvalSupported).toStrictEqual(false); expect(callArgs.isEvalSupported).not.toBeUndefined(); expect(callArgs.isEvalSupported).not.toBeNull(); expect(callArgs.isEvalSupported).not.toBe(0); }); });