7d6b4e5e48bd
AUD-002: add payload validation for SEARCH_VAULT, COPY, ML_PREDICT_BATCH
- time
- 2026-05-16 13:50 UTC · 5 months ago
- machine
- koh2
- parent
- 9c2f6f2c9d34
↓ peach-browser-7d6b4e5e.tar.gzatlas at this save
files changed2 files
src/background/messages.adversarial.test.ts+85 −0src/background/messages.ts+22 −7src/background/messages.adversarial.test.ts+85 −0
@@ -980,4 +980,89 @@980980 }981981 }982982 });983+984+ // ── AUD-002: payload validation for previously raw-cast handlers ────────985+986+ describe("AUD-002 payload validation", () => {987+ describe("SEARCH_VAULT", () => {988+ const badQueries: Array<[string, unknown]> = [989+ ["null", null],990+ ["numeric", 42],991+ ["boolean", true],992+ ["object", { evil: true }],993+ ["array", []],994+ ["missing", undefined],995+ ];996+ for (const [name, query] of badQueries) {997+ it(`rejects ${name} query`, async () => {998+ await setupUnlockedVault();999+ const r = await dispatch({1000+ type: "SEARCH_VAULT",1001+ payload: { query },1002+ } as unknown as Message);1003+ expect(r.ok).toBe(false);1004+ expect((r as { error?: string }).error).toBe("invalid_payload");1005+ });1006+ }1007+1008+ it("accepts valid string query", async () => {1009+ await setupUnlockedVault();1010+ const r = await dispatch({1011+ type: "SEARCH_VAULT",1012+ payload: { query: "github" },1013+ });1014+ expect(r.ok).toBe(true);1015+ });1016+ });1017+1018+ describe("COPY", () => {1019+ const bads: Array<[string, unknown, unknown]> = [1020+ ["null text", null, "pw"],1021+ ["numeric text", 42, "pw"],1022+ ["object text", { evil: true }, "pw"],1023+ ["missing text", undefined, "pw"],1024+ ["null label", "secret", null],1025+ ["numeric label", "secret", 42],1026+ ];1027+ for (const [name, text, label] of bads) {1028+ it(`rejects ${name}`, async () => {1029+ await setupUnlockedVault();1030+ const r = await dispatch({1031+ type: "COPY",1032+ payload: { text, label },1033+ } as unknown as Message);1034+ expect(r.ok).toBe(false);1035+ expect((r as { error?: string }).error).toBe("invalid_payload");1036+ });1037+ }1038+ });1039+1040+ describe("ML_PREDICT_BATCH", () => {1041+ const bads: Array<[string, unknown]> = [1042+ ["null", null],1043+ ["object", { not: "array" }],1044+ ["string", "features"],1045+ ["number", 42],1046+ ["missing", undefined],1047+ ];1048+ for (const [name, featuresArray] of bads) {1049+ it(`rejects ${name} featuresArray`, async () => {1050+ const r = await dispatch({1051+ type: "ML_PREDICT_BATCH",1052+ payload: { featuresArray },1053+ } as unknown as Message);1054+ expect(r.ok).toBe(false);1055+ expect((r as { error?: string }).error).toBe("invalid_payload");1056+ });1057+ }1058+1059+ it("accepts empty array featuresArray", async () => {1060+ const r = await dispatch({1061+ type: "ML_PREDICT_BATCH",1062+ payload: { featuresArray: [] },1063+ } as unknown as Message);1064+ expect((r as { error?: string }).error).not.toBe("invalid_payload");1065+ });1066+ });1067+ });9831068 });src/background/messages.ts+22 −7
@@ -656,7 +656,12 @@656656 }657657 case "SEARCH_VAULT": {658658 if (!vault.isUnlocked()) return { ok: false, error: "vault_locked" };659- const { query } = msg.payload as { query: string };659+ // AUD-002: shape-validate the payload before calling string methods.660+ // A raw cast lets a malformed message crash the handler with TypeError.661+ const { query } = (msg.payload ?? {}) as { query?: unknown };662+ if (typeof query !== "string") {663+ return { ok: false, error: "invalid_payload" };664+ }660665 const q = query.toLowerCase();661666 const searchResults = vault.getEntries().filter(662667 (e) =>@@ -782,11 +787,17 @@782787 return { ok: true, data: safeEntry };783788 }784789 case "COPY": {785- const { text, label, alreadyWritten } = msg.payload as {786- text: string;787- label: string;788- alreadyWritten?: boolean;790+ // AUD-002: shape-validate the payload before passing to the clipboard791+ // helper. A raw cast lets a malformed message reach the clipboard API792+ // with a non-string and throw.793+ const { text, label, alreadyWritten } = (msg.payload ?? {}) as {794+ text?: unknown;795+ label?: unknown;796+ alreadyWritten?: unknown;789797 };798+ if (typeof text !== "string" || typeof label !== "string") {799+ return { ok: false, error: "invalid_payload" };800+ }790801 await copyToClipboard(text, label, Boolean(alreadyWritten));791802 return { ok: true };792803 }@@ -1505,8 +1516,12 @@15051516 return { ok: true };15061517 15071518 case "ML_PREDICT_BATCH": {1508- const { featuresArray } = msg.payload as { featuresArray: MLFieldFeatures[] };1509- return { ok: true, data: mlPredictBatch(featuresArray) };1519+ // AUD-002: shape-validate before forwarding to the ML helper.1520+ const { featuresArray } = (msg.payload ?? {}) as { featuresArray?: unknown };1521+ if (!Array.isArray(featuresArray)) {1522+ return { ok: false, error: "invalid_payload" };1523+ }1524+ return { ok: true, data: mlPredictBatch(featuresArray as MLFieldFeatures[]) };15101525 }15111526 15121527 default: