32a9f43d7d6b
fix: PCH-01-017 validate verifyCode pattern, finite timestamp, and vaultBlob length in handleRestoreFromCodex before metadata reaches storage/sync-version (v3.8.1)
- time
- 2026-06-10 20:04 UTC · 4 months ago
- machine
- koh2
- agent
- claude-opus-4-8
- session
- pch-01 fix integration
- parent
- b9da7a952686
↓ peach-browser-32a9f43d.tar.gzatlas at this save
files changed2 files
src/background/codex-handlers.test.ts+46 −0src/background/codex-handlers.ts+18 −0src/background/codex-handlers.test.ts+46 −0
@@ -128,6 +128,52 @@128128 expect(unlockFromCodex).not.toHaveBeenCalled();129129 });130130 131+ // PCH-01-017: validate attacker-supplied metadata before it reaches storage.132+ it("restore_from_codex_rejects_malformed_verify_code", async () => {133+ vi.doMock("./license", () => ({ isProUser: vi.fn(async () => false) }));134+ const unlockFromCodex = vi.fn();135+ vi.doMock("./vault", () => ({ unlockFromCodex, isUnlocked: () => false, getEntries: () => [] }));136+ vi.doMock("../shared/native", () => ({ isNativePlatform: () => false, nativePlugin: () => null }));137+138+ const { handleRestoreFromCodex } = await import("./codex-handlers");139+ const result = await handleRestoreFromCodex({140+ vaultBlob: "anything",141+ timestamp: 1_700_000_000_000,142+ verifyCode: "not-a-code",143+ vaultFingerprint: "0".repeat(16),144+ entryCount: 0,145+ masterPassphrase: "ignored",146+ });147+ expect(result).toEqual({ ok: false, error: "INVALID_VERIFY_CODE" });148+ expect(unlockFromCodex).not.toHaveBeenCalled();149+ });150+151+ it("restore_from_codex_rejects_nan_timestamp_and_oversized_blob", async () => {152+ vi.doMock("./license", () => ({ isProUser: vi.fn(async () => false) }));153+ const unlockFromCodex = vi.fn();154+ vi.doMock("./vault", () => ({ unlockFromCodex, isUnlocked: () => false, getEntries: () => [] }));155+ vi.doMock("../shared/native", () => ({ isNativePlatform: () => false, nativePlugin: () => null }));156+157+ const { handleRestoreFromCodex } = await import("./codex-handlers");158+ const base = {159+ verifyCode: "PX-AAAA-BBBB",160+ vaultFingerprint: "0".repeat(16),161+ entryCount: 0,162+ masterPassphrase: "ignored",163+ };164+165+ const nanResult = await handleRestoreFromCodex({ ...base, vaultBlob: "ok", timestamp: NaN });166+ expect(nanResult).toEqual({ ok: false, error: "INVALID_VAULT_BLOB" });167+168+ const bigResult = await handleRestoreFromCodex({169+ ...base,170+ vaultBlob: "A".repeat(10 * 1024 * 1024 + 1),171+ timestamp: 1_700_000_000_000,172+ });173+ expect(bigResult).toEqual({ ok: false, error: "INVALID_VAULT_BLOB" });174+ expect(unlockFromCodex).not.toHaveBeenCalled();175+ });176+131177 it("handleRestoreFromCodex passes through stringified slot payloads without double-encoding", async () => {132178 vi.doMock("./license", () => ({ isProUser: vi.fn(async () => false) }));133179 const unlockFromCodex = vi.fn(async () => ({ ok: true as const, entryCount: 1 }));src/background/codex-handlers.ts+18 −0
@@ -27,11 +27,16 @@2727 STORAGE_KEY_LAST_RESTORED,2828 STORAGE_KEY_VERIFY_CODE,2929 } from "../shared/codex-types";30+import { IMPORT_LIMITS } from "../shared/import-limits";3031 3132 // ── Verify-code helpers ─────────────────────────────────────────────────────3233 3334 const VERIFY_CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; // no I/O/0/13435 36+// PCH-01-017: same pattern enforced popup-side in helpers.ts. Keep these in37+// sync — ideally both should import one shared constant (follow-up refactor).38+const VERIFY_CODE_PATTERN = /^PX-[A-Z0-9]{4}-[A-Z0-9]{4}$/;39+3540 /**3641 * Format: `PX-XXXX-XXXX`. 8 random Crockford-ish characters split into two3742 * groups of four. Stable identifier for a vault across Codex prints — once@@ -225,6 +230,19 @@225230 error?: string;226231 data?: { entryCount: number; verifyCode: string };227232 }> {233+ // PCH-01-017: validate attacker-supplied metadata before it can reach storage234+ // or sync-version comparisons. The trial-decrypt gate in unlockFromCodex235+ // protects the vault bytes; these checks protect the metadata riding alongside.236+ if (!VERIFY_CODE_PATTERN.test(payload.verifyCode)) {237+ return { ok: false, error: "INVALID_VERIFY_CODE" };238+ }239+ if (!Number.isFinite(payload.timestamp)) {240+ return { ok: false, error: "INVALID_VAULT_BLOB" };241+ }242+ if (payload.vaultBlob.length > IMPORT_LIMITS.MAX_JSON_BYTES) {243+ return { ok: false, error: "INVALID_VAULT_BLOB" };244+ }245+228246 // Parse the base64url-wrapped JSON { a, b }. Malformed input is a hard229247 // refusal — we will not write garbage into slotA.230248 let slotAJson: string;