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
2 files src/background/codex-handlers.test.ts+46 −0src/background/codex-handlers.ts+18 −0
src/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 }));

view file

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;

view file