ddca1d438ac7

M10/COR-012: validate incoming vault blob shape before writing

time
2026-05-16 03:07 UTC · 5 months ago
machine
koh2
parent
47c6f243081f
1 file src/background/vault.ts+31 −16
src/background/vault.ts+31 −16
@@ -1177,21 +1177,18 @@11771177   let parsedSlotA: StoredVault;11781178   let parsedSlotB: StoredVault | null = null;11791179   try {1180-    parsedSlotA = JSON.parse(params.slotA) as StoredVault;1181-    if (params.slotB !== null) {1182-      parsedSlotB = JSON.parse(params.slotB) as StoredVault;1183-    }1184-    // Minimal structural validation — the decrypt step below catches deeper1185-    // corruption, but bail early on obviously-bad shapes so we don't reach1186-    // storage with junk.1187-    if (1188-      typeof parsedSlotA?.salt !== "string" ||1189-      typeof parsedSlotA?.nonce !== "string" ||1190-      typeof parsedSlotA?.ciphertext !== "string" ||1191-      typeof parsedSlotA?.tag !== "string"1192-    ) {1180+    const rawA = JSON.parse(params.slotA) as unknown;1181+    if (!hasValidVaultSchema(rawA)) {11931182       return { ok: false, reason: "invalid_slot_format" };11941183     }1184+    parsedSlotA = rawA;1185+    if (params.slotB !== null) {1186+      const rawB = JSON.parse(params.slotB) as unknown;1187+      if (!hasValidVaultSchema(rawB)) {1188+        return { ok: false, reason: "invalid_slot_format" };1189+      }1190+      parsedSlotB = rawB;1191+    }11951192   } catch {11961193     return { ok: false, reason: "invalid_slot_format" };11971194   }@@ -2241,12 +2238,30 @@22412238 22422239   if (remoteTimestamp <= localTimestamp) return false;22432240 2241+  // Parse + shape-validate before any storage mutation. A malformed blob2242+  // (non-JSON, non-object, missing slot fields) must NOT touch slot A —2243+  // otherwise an unauthenticated remote could poison the local vault by2244+  // sending '[]' or '42' (M10/COR-012). Mirror the structural check2245+  // already used in unlockFromCodex.2246+  let parsed: unknown;2247+  try {2248+    parsed = JSON.parse(blobJson);2249+  } catch {2250+    throw new Error('invalid_incoming_vault_blob');2251+  }2252+  if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {2253+    throw new Error('invalid_incoming_vault_blob');2254+  }2255+  const { a, b } = parsed as { a?: unknown; b?: unknown };2256+  if (!hasValidVaultSchema(a)) {2257+    throw new Error('invalid_incoming_vault_blob');2258+  }2259+22442260   // Snapshot before overwriting — failure is non-fatal22452261   await takeVaultSnapshot('sync_received').catch(() => {});22462262 2247-  const { a, b } = JSON.parse(blobJson) as { a: unknown; b?: unknown };2248-  await writeSlot(SLOT_A, a as StoredVault);2249-  if (b) await writeSlot(SLOT_B, b as StoredVault);2263+  await writeSlot(SLOT_A, a);2264+  if (hasValidVaultSchema(b)) await writeSlot(SLOT_B, b);22502265   await chrome.storage.local.set({22512266     peach_last_sync_at: String(Date.now()),22522267     peach_last_modified_at: String(remoteTimestamp),

view file