3b81fc8ed88e
M2/QUA-002: add runtime payload validation at message handler trust boundary
- time
- 2026-05-16 03:55 UTC · 5 months ago
- machine
- koh2
- parent
- 13f21e71dd16
↓ peach-browser-3b81fc8e.tar.gzatlas at this save
files changed2 files
src/background/messages.adversarial.test.ts+48 −0src/background/messages.ts+55 −17src/background/messages.adversarial.test.ts+48 −0
@@ -932,4 +932,52 @@932932 });933933 }934934 });935+936+ // ── M2/QUA-002: payload validation at trust boundary ────────────────────937+938+ describe("payload validation rejects malformed inputs (M2/QUA-002)", () => {939+ // Each entry: handler type + payloads that must produce ok:false.940+ // The accepted error codes include 'invalid_payload' (the new validator)941+ // or 'vault_locked' (handlers that lock-check before validating, which942+ // is still a correct rejection of the request).943+ const cases: Array<{ type: string; bad: unknown[] }> = [944+ {945+ type: "ADD_ENTRY",946+ bad: [null, "string", 42, [], true],947+ },948+ {949+ type: "BATCH_ADD_ENTRIES",950+ bad: [null, { entries: "not-array" }, { entries: null }, { entries: 42 }, []],951+ },952+ {953+ type: "UPDATE_ENTRY",954+ bad: [null, {}, { id: 123 }, { id: "x" }, { id: "x", patch: "no" }, { id: "x", patch: null }, []],955+ },956+ {957+ type: "RESTORE_ENTRIES",958+ bad: [null, {}, { entries: "no" }, { entries: [], mode: "wrong" }, []],959+ },960+ {961+ type: "UPDATE_SETTINGS",962+ bad: [null, "string", 42, []],963+ },964+ {965+ type: "REMOVE_TRUSTED_NETWORK",966+ bad: [null, "string", 42, []],967+ },968+ ];969+970+ for (const { type, bad } of cases) {971+ for (const payload of bad) {972+ const label = JSON.stringify(payload);973+ it(`${type} rejects malformed payload: ${label}`, async () => {974+ await setupUnlockedVault();975+ const r = await dispatch({ type, payload } as unknown as Message);976+ expect(r.ok).toBe(false);977+ const err = (r as { error?: string }).error ?? "";978+ expect(err).toMatch(/invalid_payload|vault[_ ]locked/);979+ });980+ }981+ }982+ });935983 });src/background/messages.ts+55 −17
@@ -530,24 +530,45 @@530530 return { ok: true, data: vault.getEntries() };531531 case "ADD_ENTRY": {532532 if (!vault.isUnlocked()) return { ok: false, error: "vault locked" };533- const entry = await vault.addEntry(msg.payload as any);533+ const payload = msg.payload;534+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {535+ return { ok: false, error: "invalid_payload" };536+ }537+ const entry = await vault.addEntry(payload as Parameters<typeof vault.addEntry>[0]);534538 return { ok: true, data: entry };535539 }536540 case "BATCH_ADD_ENTRIES": {537- const { entries } = msg.payload as { entries: any[] };538- const added = await vault.batchAddEntries(entries);541+ const payload = msg.payload as { entries?: unknown } | null | undefined;542+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {543+ return { ok: false, error: "invalid_payload" };544+ }545+ if (!Array.isArray(payload.entries)) {546+ return { ok: false, error: "invalid_payload" };547+ }548+ const added = await vault.batchAddEntries(payload.entries as Parameters<typeof vault.batchAddEntries>[0]);539549 return { ok: true, data: added };540550 }541551 case "RESTORE_ENTRIES": {542- const { entries, mode, settings } = msg.payload as {543- entries: any[];544- mode: "replace" | "merge";545- settings?: vault.VaultSettings;546- };547- const summary = await vault.restoreEntries(entries, mode);548- if (settings && typeof settings === "object") {549- await vault.updateSettings(settings);552+ const payload = msg.payload as553+ | { entries?: unknown; mode?: unknown; settings?: unknown }554+ | null555+ | undefined;556+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {557+ return { ok: false, error: "invalid_payload" };550558 }559+ if (!Array.isArray(payload.entries)) {560+ return { ok: false, error: "invalid_payload" };561+ }562+ if (payload.mode !== "replace" && payload.mode !== "merge") {563+ return { ok: false, error: "invalid_payload" };564+ }565+ const summary = await vault.restoreEntries(566+ payload.entries as Parameters<typeof vault.restoreEntries>[0],567+ payload.mode568+ );569+ if (payload.settings && typeof payload.settings === "object" && !Array.isArray(payload.settings)) {570+ await vault.updateSettings(payload.settings as vault.VaultSettings);571+ }551572 return { ok: true, data: summary };552573 }553574 case "CLEAR_VAULT_ENTRIES": {@@ -573,8 +594,15 @@573594 return { ok: true, data: removed };574595 }575596 case "UPDATE_ENTRY": {576- const { id, patch } = msg.payload as any;577- await vault.updateEntry(id, patch);597+ const payload = msg.payload as { id?: unknown; patch?: unknown } | null | undefined;598+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {599+ return { ok: false, error: "invalid_payload" };600+ }601+ const { id, patch } = payload;602+ if (typeof id !== "string" || !id || !patch || typeof patch !== "object" || Array.isArray(patch)) {603+ return { ok: false, error: "invalid_payload" };604+ }605+ await vault.updateEntry(id, patch as Parameters<typeof vault.updateEntry>[1]);578606 return { ok: true };579607 }580608 case "ADD_ADDITIONAL_URL": {@@ -801,9 +829,12 @@801829 case "GET_SETTINGS":802830 return { ok: true, data: await vault.ensureSettingsLoaded() };803831 case "UPDATE_SETTINGS": {804- const patch = msg.payload as Record<string, unknown>;805- await vault.updateSettings(patch as any);806- if (typeof patch.autoLockMinutes === "number") {832+ const patch = msg.payload;833+ if (!patch || typeof patch !== "object" || Array.isArray(patch)) {834+ return { ok: false, error: "invalid_payload" };835+ }836+ await vault.updateSettings(patch as vault.VaultSettings);837+ if (typeof (patch as Record<string, unknown>).autoLockMinutes === "number") {807838 await setupAutoLock();808839 if (vault.isUnlocked()) touchAutoLock();809840 }@@ -1046,7 +1077,14 @@10461077 return { ok: true };10471078 }10481079 case "REMOVE_TRUSTED_NETWORK": {1049- const { ssid, bssid, isTailscale } = msg.payload as any;1080+ const payload = msg.payload as1081+ | { ssid?: unknown; bssid?: unknown; isTailscale?: unknown }1082+ | null1083+ | undefined;1084+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {1085+ return { ok: false, error: "invalid_payload" };1086+ }1087+ const { ssid, bssid, isTailscale } = payload as { ssid?: string; bssid?: string; isTailscale?: boolean };10501088 const networks = await getTrustedNetworks();10511089 const network = networks.find((n: { ssid?: string; bssid?: string; isTailscale?: boolean }) => 10521090 (isTailscale ? n.isTailscale : n.ssid === ssid) &&