b9da7a952686

fix: PCH-01-016 run sanitizeCanary + new bounded sanitizeTags inside sanitizeEntryDraft (and restore path) — closes MV-6 whitelist bypass on batch-add/import (v3.8.1)

time
2026-06-10 20:03 UTC · 4 months ago
machine
koh2
agent
claude-opus-4-8
session
pch-01 fix integration
parent
c85d657224cc
2 files src/background/vault.adversarial.test.ts+39 −0src/background/vault.ts+25 −2
src/background/vault.adversarial.test.ts+39 −0
@@ -236,6 +236,45 @@236236       expect(r.ok).toBe(false);237237     });238238 239+    // PCH-01-016: the batch-add path was skipping the MV-6 canary/tags whitelist.240+    it("batch_add_drops_non_https_canary", async () => {241+      const vault = await import("./vault");242+      await vault.createVault("test-pass-12345");243+      const [entry] = await vault.batchAddEntries([244+        {245+          type: "login",246+          title: "Acct",247+          username: "u",248+          password: "p",249+          url: "https://e.com",250+          canary: { webhookUrl: "http://evil.example" },251+        } as unknown as Parameters<typeof vault.batchAddEntries>[0][number],252+      ]);253+      expect(entry.canary).toBeUndefined();254+    });255+256+    it("batch_add_filters_oversized_and_nonstring_tags", async () => {257+      const vault = await import("./vault");258+      await vault.createVault("test-pass-12345");259+      const tags = [123, "  ", "x".repeat(5000), ...Array(1000).fill("t")];260+      const [entry] = await vault.batchAddEntries([261+        {262+          type: "login",263+          title: "Tagged",264+          username: "u",265+          password: "p",266+          url: "https://e.com",267+          tags,268+        } as unknown as Parameters<typeof vault.batchAddEntries>[0][number],269+      ]);270+      expect(entry.tags).toBeDefined();271+      expect(entry.tags!.length).toBeLessThanOrEqual(255);272+      expect(entry.tags!.every((t) => typeof t === "string")).toBe(true);273+      expect(entry.tags!.every((t) => t.length <= 256)).toBe(true);274+      // 123 and "  " dropped; "x".repeat(5000) truncated to 256 chars.275+      expect(entry.tags).toContain("x".repeat(256));276+    });277+239278     const titleCases = [240279       { name: "1 char title", title: "x" },241280       { name: "100 char title", title: "x".repeat(100) },

view file

src/background/vault.ts+25 −2
@@ -1620,6 +1620,8 @@16201620   if (normalized.canary) {16211621     normalized.canary = sanitizeCanary(normalized.canary);16221622   }1623+  // PCH-01-016: bound tags on the restore path too, so both import funnels agree.1624+  normalized.tags = sanitizeTags(normalized.tags);16231625 16241626   // Login entries from backup must carry a password (or be passkey-only,16251627   // in which case the passkey sub-object replaces the password requirement).@@ -1711,6 +1713,25 @@17111713   };17121714 }17131715 1716+/**1717+ * MV-6 / PCH-01-016: tags from imported / batch-added entries are untrusted.1718+ * Keep only non-empty strings, cap each at 256 chars, and cap the count at 2551719+ * (the same per-entry ceiling the export codec enforces, peachPayload.ts).1720+ * Non-array or all-invalid input collapses to undefined.1721+ */1722+function sanitizeTags(tags: unknown): string[] | undefined {1723+  if (!Array.isArray(tags)) return undefined;1724+  const out: string[] = [];1725+  for (const t of tags) {1726+    if (typeof t !== "string") continue;1727+    const trimmed = t.trim();1728+    if (!trimmed) continue;1729+    out.push(trimmed.slice(0, 256));1730+    if (out.length >= 255) break;1731+  }1732+  return out.length > 0 ? out : undefined;1733+}1734+17141735 // Soft-deletes every non-deleted entry in one call. The literal string17151736 // argument is a tripwire against accidental calls — any caller that would17161737 // wipe the vault has to consciously type "REPLACE_ALL". Do not relax this@@ -2264,8 +2285,10 @@22642285     totp: type === "login" ? entry.totp?.trim() || undefined : undefined,22652286     notes: entry.notes?.trim() || undefined,22662287     breach: type === "login" ? entry.breach : undefined,2267-    canary: type === "login" ? entry.canary : undefined,2268-    tags: entry.tags,2288+    // PCH-01-016: the batch-add path was bypassing the MV-6 canary/tags2289+    // whitelist applied on the RESTORE_ENTRIES path. Run both sanitizers here.2290+    canary: type === "login" ? sanitizeCanary(entry.canary) : undefined,2291+    tags: sanitizeTags(entry.tags),22692292     passkey: entry.passkey,22702293     cardholderName: type === "card" ? entry.cardholderName?.trim() : undefined,22712294     cardNumber: type === "card" ? entry.cardNumber?.trim() : undefined,

view file