a207e6c140c2
fix: MV-5 file and string size limits on all import paths — PDF gate before read, raw-string caps in parseImportSource and importEntriesFromPeachJson
- time
- 2026-06-10 02:16 UTC · 4 months ago
- machine
- koh2
- agent
- claude-opus-4-8
- session
- audit fix integration
- parent
- 386e53180a98
↓ peach-browser-a207e6c1.tar.gzatlas at this save
files changed6 files
src/popup/codex/pdf-import.test.ts+20 −1src/popup/codex/pdf-import.ts+15 −0src/shared/backup.test.ts+7 −0src/shared/backup.ts+8 −0src/shared/import.test.ts+5 −0src/shared/import.ts+8 −0src/popup/codex/pdf-import.test.ts+20 −1
@@ -28,7 +28,7 @@2828 * has automated coverage.2929 */3030 31-import { describe, expect, it } from "vitest";31+import { describe, expect, it, vi } from "vitest";3232 3333 import type { BitMatrix } from "@zxing/library";3434 import { buildAztecMatrix } from "./aztec";@@ -359,4 +359,23 @@359359 expect(parsed!.d).toBe(envelopes[i]!.d);360360 }361361 }, 30_000);362+363+ it("MV-5: importCodexPDF rejects an oversized file without reading it", async () => {364+ const { importCodexPDF } = await import("./pdf-import");365+ const { IMPORT_LIMITS } = await import("../../shared/import-limits");366+ const arrayBuffer = vi.fn(async () => new ArrayBuffer(0));367+ const fakeFile = {368+ size: IMPORT_LIMITS.MAX_PDF_BYTES + 1,369+ arrayBuffer,370+ } as unknown as File;371+372+ const result = await importCodexPDF(fakeFile);373+374+ expect(result.chunksFound).toBe(0);375+ expect(result.envelopes).toEqual([]);376+ expect(result.errors).toHaveLength(1);377+ expect(result.errors[0]).toMatch(/limited to/);378+ // Never read into memory — the gate fires before file.arrayBuffer().379+ expect(arrayBuffer).not.toHaveBeenCalled();380+ });362381 });src/popup/codex/pdf-import.ts+15 −0
@@ -39,6 +39,7 @@3939 type ChunkEnvelope,4040 } from "../../shared/codex-types";4141 import { verifyChunkIntegrity } from "./helpers";42+import { IMPORT_LIMITS } from "../../shared/import-limits";4243 import {4344 CODE_SIZE,4445 COL_GAP,@@ -244,6 +245,20 @@244245 file: File,245246 onProgress?: PDFImportProgress,246247 ): Promise<PDFImportResult> {248+ // MV-5: refuse oversized files before reading them into memory — and before249+ // even loading pdf.js. A real Codex tops out well under this (200 pages ≈250+ // 4 MiB of Aztec PNGs).251+ if (file.size > IMPORT_LIMITS.MAX_PDF_BYTES) {252+ return {253+ chunksFound: 0,254+ totalExpected: null,255+ envelopes: [],256+ errors: [257+ `PDF is ${(file.size / (1024 * 1024)).toFixed(1)} MiB — Codex PDFs are limited to ${IMPORT_LIMITS.MAX_PDF_BYTES / (1024 * 1024)} MiB.`,258+ ],259+ };260+ }261+247262 const pdfjsLib = await getPdfJs();248263 249264 const envelopes: ChunkEnvelope[] = [];src/shared/backup.test.ts+7 −0
@@ -95,4 +95,11 @@9595 importEntriesFromPeachJson(JSON.stringify(envelope), "export-pass")9696 ).rejects.toThrow("INVALID_PEACH_EXPORT");9797 });98+99+ it("MV-5: importEntriesFromPeachJson rejects an over-limit raw string with IMPORT_TOO_LARGE", async () => {100+ const huge = "x".repeat(11 * 1024 * 1024); // 11 MiB > 10 MiB cap101+ await expect(importEntriesFromPeachJson(huge, "export-pass")).rejects.toThrow(102+ /IMPORT_TOO_LARGE/,103+ );104+ });98105 });src/shared/backup.ts+8 −0
@@ -1,4 +1,5 @@11 import { decrypt, encrypt } from "../background/aes";2+import { IMPORT_LIMITS, importTooLarge } from "./import-limits";23 import type { PeachLicenseExport, VaultEntry } from "./types";34 45 /**@@ -54,6 +55,13 @@5455 raw: string,5556 password: string5657 ): Promise<PeachJsonImportResult> {58+ // MV-5: defense in depth — parseImportSource caps first when this is59+ // reached through it, but this function is also exported directly.60+ if (raw.length > IMPORT_LIMITS.MAX_JSON_BYTES) {61+ throw importTooLarge(62+ `peach export ${raw.length} chars exceeds ${IMPORT_LIMITS.MAX_JSON_BYTES}`,63+ );64+ }5765 let envelope: Partial<PeachExportEnvelope>;5866 try {5967 envelope = JSON.parse(raw) as Partial<PeachExportEnvelope>;src/shared/import.test.ts+5 −0
@@ -467,4 +467,9 @@467467 expect(result.entries[0].totp).toBe("TEST123");468468 expect(result.entries[0].notes).toBe("A note");469469 });470+471+ it("MV-5: parseImportSource rejects an over-limit raw string with IMPORT_TOO_LARGE", async () => {472+ const huge = "x".repeat(11 * 1024 * 1024); // 11 MiB > 10 MiB cap473+ await expect(parseImportSource(huge)).rejects.toThrow(/IMPORT_TOO_LARGE/);474+ });470475 });src/shared/import.ts+8 −0
@@ -13,6 +13,7 @@1313 importPasskeyPrivateKey,1414 } from "../background/passkey-crypto";1515 import { parseOtpauthURI } from "../crypto/totp";16+import { IMPORT_LIMITS, importTooLarge } from "./import-limits";1617 import { importEntriesFromPeachJson, type ImportableEntry } from "./backup";1718 1819 export type ImportFormat =@@ -41,6 +42,13 @@4142 raw: string,4243 password?: string4344 ): Promise<ImportParseResult> {45+ // MV-5: bound the input before any JSON.parse / CSV parse allocates46+ // proportional structures. Covers file picks AND pasted strings.47+ if (raw.length > IMPORT_LIMITS.MAX_JSON_BYTES) {48+ throw importTooLarge(49+ `import source ${raw.length} chars exceeds ${IMPORT_LIMITS.MAX_JSON_BYTES}`,50+ );51+ }4452 const format = detectImportFormat(raw);4553 4654 switch (format) {