pdf-import.sandbox.test.ts
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});