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
↓ peach-browser-ddca1d43.tar.gzatlas at this save
files changed1 file
src/background/vault.ts+31 −16src/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),