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
↓ peach-browser-3007c77c.tar.gzatlas at this save
files changed8 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 −9manifest.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": {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",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",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",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");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 thesrc/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;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";