1# PCH-02-001 — UPDATE_ENTRY patch path routes canary/tags/passkey through existing sanitizers23**Severity:** Low (the only non-Informational finding in PCH-02)4**Target file:** `src/background/vault.ts` — `sanitizeEntryPatch`, lines 2375–23885**Report:** peach-code-review.md, Finding PCH-02-00167## Problem89`sanitizeEntryPatch` copies `patch.canary` (2375–2377), `patch.passkey`10(2379–2384), and `patch.tags` (2386–2388) through verbatim, while every other11vault-entry funnel rebuilds those shapes field-by-field:1213- add / batch-add → `sanitizeEntryDraft` (vault.ts:2256–2315) runs14 `sanitizeCanary` + `sanitizeTags` (PCH-01-016) and throws on invalid passkey15- restore / import / sync → `sanitizeRestoredEntry` (vault.ts:1603–1650) runs16 `sanitizePasskey` + `sanitizeCanary` + `sanitizeTags` (MV-6)1718`UPDATE_ENTRY` is legitimately sent from content-script context (inline editor,19canary-ping bump at `content/index.ts:336–348`), so it cannot be added to20`DESTRUCTIVE_MESSAGE_TYPES` — the fix is to make the patch path use the same21sanitizers, not to gate the sender.2223## Fix — exact edit2425In `sanitizeEntryPatch` (vault.ts), replace the three verbatim branches:2627```typescript2829 if ("canary" in patch) {30 nextPatch.canary = patch.canary;31 }3233 if ("passkey" in patch) {34 35 36 37 nextPatch.passkey = patch.passkey;38 }3940 if ("tags" in patch) {41 nextPatch.tags = patch.tags;42 }43```4445```typescript4647 if ("canary" in patch) {48 49 50 51 52 nextPatch.canary = patch.canary ? sanitizeCanary(patch.canary) : undefined;53 }5455 if ("passkey" in patch) {56 57 58 59 60 61 62 63 if (patch.passkey === undefined || patch.passkey === null) {64 nextPatch.passkey = undefined;65 } else {66 const sanitized = sanitizePasskey(patch.passkey);67 if (!sanitized) throw new Error("invalid passkey credential");68 nextPatch.passkey = sanitized;69 }70 }7172 if ("tags" in patch) {73 74 75 76 nextPatch.tags = sanitizeTags(patch.tags);77 }78```7980No import changes needed — `sanitizeCanary`, `sanitizePasskey`, and81`sanitizeTags` are module-local functions in the same file (vault.ts:1657,821705, 1730).8384## Compatibility audit of every existing UPDATE_ENTRY producer8586Verified against source before choosing these semantics:8788| Producer | Patch shape | Survives? |89|---|---|---|90| Content canary-ping bump (`content/index.ts:336–348`) | full existing canary + `lastPinged`/`pingCount` | Yes — `sanitizeCanary` preserves `webhookUrl`/`label`/`lastPinged`/`pingCount`; the stored webhook is already https |91| Popup/inline edit forms | scalar fields, full passkey object on edit | Yes — complete `PasskeyData` passes `sanitizePasskey` |92| "Remove passkey" UX | `passkey: undefined` | Yes — explicit removal branch kept |93| `ADD_ADDITIONAL_URL` handler (messages.ts:510) | `{ additionalUrls }` | Untouched branch |94| `SAVE_NOTE` handler (messages.ts:542) | `{ title, notes, modifiedAt }` | Untouched branches (`modifiedAt` was already ignored by the whitelist) |95| Breach updater (messages.ts:839) | `{ breach }` | Untouched branch |9697Note on URL normalization: `sanitizeCanary` round-trips the webhook through98`new URL(...).toString()`, so `https://x.example.com` persists as99`https://x.example.com/`. This already happens on the add/restore paths; the100patch path simply joins them.101102## messages.ts handler verification (no bypass)103104`UPDATE_ENTRY` (messages.ts:478–501) has exactly one mutation path:105`vault.updateEntry(id, patch)` → `sanitizeEntryPatch` (vault.ts:1836). There is106no second entry point that skips the sanitizer. The Pro gate for *new* canaries107(messages.ts:491–498) and the fire-time SSRF guard108(`isAllowedCanaryWebhookUrl`, messages.ts:1568) remain as the outer gates.109110**Adjacent observation (no change made):** `vault.updateEntryDirect`111(vault.ts:1978) spreads a patch verbatim, but it has **zero production112callers** (grep: only its definition; the sync path uses113`applyMergedRemoteEntries`, which funnels through `sanitizeRestoredEntry`).114Flagged for a future dead-code pass; out of scope for this finding.115116## Tests117118`vault.update-entry-sanitize.test.ts` (in this folder) — 8 regression tests.119Destination: `src/background/vault.update-entry-sanitize.test.ts` (imports are120already in destination form).121122## Save message (integration)123124`fix: PCH-02-001 UPDATE_ENTRY patch routes canary/tags/passkey through existing sanitizers`