3007c77c652e

fix: stop passive global identity disclosure (PB-26-202, v5.0.57)

time
2026-09-02 19:55 UTC · 1 month ago
machine
koh2
agent
gpt-5.6-luna
session
PB-26-202 identity disclosure
parent
a7ed252fcbef
8 files manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/background/message-policy.test.ts+23 −0src/background/messages.test.ts+26 −0src/content/content.field-interaction.test.ts+1 −0src/content/index.ts+2 −11src/content/inject.ts+1 −9
manifest.firefox.json+1 −1
@@ -1,7 +1,7 @@11 {22   "manifest_version": 3,33   "name": "Peach Password Manager",4-  "version": "5.0.56",4+  "version": "5.0.57",55   "description": "Zero-knowledge password manager",66   "browser_specific_settings": {77     "gecko": {

view file

manifest.json+1 −1
@@ -2,7 +2,7 @@22   "manifest_version": 3,33   "minimum_chrome_version": "116",44   "name": "Peach Password Manager",5-  "version": "5.0.56",5+  "version": "5.0.57",66   "description": "Zero-knowledge password manager",77   "action": {88     "default_popup": "src/popup/index.html",

view file

package.json+1 −1
@@ -1,6 +1,6 @@11 {22   "name": "peach",3-  "version": "5.0.56",3+  "version": "5.0.57",44   "description": "Zero-knowledge password manager",55   "license": "GPL-3.0-only",66   "type": "module",

view file

src/background/message-policy.test.ts+23 −0
@@ -73,6 +73,29 @@7373     }7474   });7575 76+  it("keeps the global identity inventory extension-page-only", () => {77+    expect(getMessageContext("GET_IDENTITIES")).toBe("extension-page-only");78+79+    const extensionOrigin = "chrome-extension://peach-extension/";80+    expect(81+      isMessageSenderAllowed(82+        "GET_IDENTITIES",83+        { id: "peach-extension", url: `${extensionOrigin}popup.html` },84+        "peach-extension",85+        extensionOrigin,86+      ),87+    ).toBe(true);88+89+    for (const sender of [90+      { id: "peach-extension", url: "https://example.test/signup" },91+      { id: "peach-extension", url: "https://example.test/signup", tab: { id: 1 } },92+    ]) {93+      expect(94+        isMessageSenderAllowed("GET_IDENTITIES", sender, "peach-extension", extensionOrigin),95+      ).toBe(false);96+    }97+  });98+7699   it("covers every message type against popup, content-script, and foreign senders", () => {77100     const popup = {78101       id: "peach-extension",

view file

src/background/messages.test.ts+26 −0
@@ -814,6 +814,32 @@814814     expect(hits[0]!.useCount).toBe(3);815815   });816816 817+  it("GET_IDENTITIES rejects content-script and page-relay senders", async () => {818+    const vault = await import("./vault");819+    await vault.createVault("correct-pass-12345");820+    await vault.addEntry({821+      title: "Private identity",822+      username: "[email protected]",823+      password: "pw",824+      url: "https://private.example",825+    });826+827+    const senders = [828+      {829+        id: "peach-extension",830+        url: "https://private.example/signup",831+        tab: { id: 1 } as chrome.tabs.Tab,832+      },833+      { id: "peach-extension", url: "https://private.example/signup" },834+    ] satisfies chrome.runtime.MessageSender[];835+    for (const sender of senders) {836+      await expect(dispatch({ type: "GET_IDENTITIES" }, sender)).resolves.toMatchObject({837+        ok: false,838+        error: "invalid_sender",839+      });840+    }841+  });842+817843   it("IS_CARD_DECLINED does not refresh the auto-lock timer", async () => {818844     const vault = await import("./vault");819845     await vault.createVault("correct-pass-12345");

view file

src/content/content.field-interaction.test.ts+1 −0
@@ -124,6 +124,7 @@124124     await vi.waitFor(() => expect(showSelector).toHaveBeenCalled());125125     expect(lastSelectorOptions()?.isSignup).toBe(true);126126     expect(lastSelectorOptions()?.locked).toBeFalsy();127+    expect(sentMessages.some((msg) => msg.type === "GET_IDENTITIES")).toBe(false);127128   });128129 129130   // T-F05: an email-first form (no password on this step) still opens on the

view file

src/content/index.ts+2 −11
@@ -33,7 +33,7 @@3333 import { contentMLDetector } from "./content-ml-detector";3434 import { detectPasswordPolicy } from "./policy";3535 import type { FieldPrediction } from "../shared/ml";36-import type { SavedIdentity, VaultEntry } from "../shared/types";36+import type { VaultEntry } from "../shared/types";3737 3838 // WeakMap (not WeakSet) — we keep the per-input cleanup callback as the value3939 // so we can tear down listeners if we ever need to re-process the same field.@@ -453,18 +453,10 @@453453   capability?: string,454454 ): Promise<void> {455455   // No saved entries for this site → signup/new-account mode.456-  // Fetch vault identities for the identity-first surface and locate457-  // the confirm-password field so "Copy & fill" can populate it too.456+  // Locate the confirm-password field so "Copy & fill" can populate it too.458457   const isSignup = domainEntries.length === 0;459-  let savedIdentities: SavedIdentity[] | undefined;460458   let confirmEl: HTMLInputElement | null = null;461459   if (isSignup) {462-    try {463-      const idResp = await chrome.runtime.sendMessage({ type: "GET_IDENTITIES" }).catch(() => null) as464-        { ok: boolean; data?: SavedIdentity[] } | null;465-      if (idResp?.ok && Array.isArray(idResp.data)) savedIdentities = idResp.data;466-    } catch { /* silent fallback */ }467-468460     const form = (passwordEl ?? usernameEl)?.closest("form") ?? document.body;469461     const allPwInputs = Array.from(form.querySelectorAll<HTMLInputElement>("input[type='password']"));470462     confirmEl = allPwInputs.find(@@ -510,7 +502,6 @@510502       isSignup,511503       identityKind,512504       hasEmailField: !!emailEl,513-      savedIdentities,514505       onFillFields: (username, password, email) => {515506         const form = (passwordEl ?? usernameEl)?.closest("form") as HTMLFormElement | null ?? null;516507         if (!fillFields(usernameEl, passwordEl, username, password, form, getPageDomain(), emailEl, email)) return;

view file

src/content/inject.ts+1 −9
@@ -2411,15 +2411,7 @@24112411     // regenerate click so the user always gets a compliant password.24122412     const sitePolicy = detectPasswordPolicy();24132413 2414-    // Fetch identities on-demand — they're passed in signup context but not2415-    // in login/search context where the user still clicks "New entry".2416-    let formIdentities: SavedIdentity[] = options.savedIdentities ?? [];2417-    if (!formIdentities.length) {2418-      try {2419-        const idRes = await chrome.runtime.sendMessage({ type: "GET_IDENTITIES" }).catch(() => null) as { ok: boolean; data?: SavedIdentity[] } | null;2420-        if (idRes?.ok && Array.isArray(idRes.data)) formIdentities = idRes.data;2421-      } catch { /* silent fallback */ }2422-    }2414+    const formIdentities = options.savedIdentities ?? [];24232415 24242416     const wrap = document.createElement("div");24252417     wrap.className = "create-form-wrap";

view file