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
2 files src/background/messages.adversarial.test.ts+85 −0src/background/messages.ts+22 −7
src/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 });

view file

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:

view file