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
↓ peach-browser-b9da7a95.tar.gzatlas at this save
files changed2 files
src/background/vault.adversarial.test.ts+39 −0src/background/vault.ts+25 −2src/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) },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,