bdcc99ca7c46

fix: show trustworthy origins for credential decisions (PB-26-707, PB-26-710, v5.2.12)

time
2026-09-07 01:23 UTC · 1 month ago
machine
koh2
agent
gpt-5
session
red team remediation
parent
80f09eb7e4bd
9 files manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/content/content.selector-ui.test.ts+20 −0src/content/index.ts+1 −1src/content/inject.ts+16 −9src/popup/App.tsx+4 −4src/shared/domain.homograph.test.ts+12 −1src/shared/domain.ts+12 −0
manifest.firefox.json+1 −1
@@ -1,7 +1,7 @@11 {22   "manifest_version": 3,33   "name": "Peach Password Manager",4-  "version": "5.2.11",4+  "version": "5.2.12",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.2.11",5+  "version": "5.2.12",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.2.11",3+  "version": "5.2.12",44   "description": "Zero-knowledge password manager",55   "license": "GPL-3.0-only",66   "type": "module",

view file

src/content/content.selector-ui.test.ts+20 −0
@@ -40,6 +40,26 @@4040 });4141 4242 describe("T-I01: mount + dismiss (the observable contract)", () => {43+  it("PB-26-707: shows the full security origin instead of the registrable domain", () => {44+    const originalAttachShadow = Element.prototype.attachShadow;45+    const attachSpy = vi.spyOn(Element.prototype, "attachShadow").mockImplementation(function (this: Element) {46+      return originalAttachShadow.call(this, { mode: "open" });47+    });48+    const anchor = makeInput({ type: "password" });49+50+    try {51+      const dismiss = showAutofillSelector(anchor, ENTRIES, vi.fn(), {52+        domain: "example.com",53+        securityOrigin: "https://login.accounts.example.com:8443",54+      });55+      expect(selectorHost()?.shadowRoot?.querySelector(".domain-name")?.textContent)56+        .toBe("https://login.accounts.example.com:8443");57+      dismiss();58+    } finally {59+      attachSpy.mockRestore();60+    }61+  });62+4363   // T-I01: opening the selector mounts a host into the document and returns a4464   // dismiss function.4565   it("T-I01: showAutofillSelector mounts a host and returns a dismiss fn", () => {

view file

src/content/index.ts+1 −1
@@ -491,7 +491,7 @@491491           } catch { /* ignore unparseable */ }492492         }493493         if (pageDomain && entryDomains.size > 0 && !entryDomains.has(pageDomain)) {494-          showRememberSiteToast(pageDomain, entry.id);494+          showRememberSiteToast(location.origin, entry.id);495495         }496496       }497497     },

view file

src/content/inject.ts+16 −9
@@ -10,6 +10,7 @@1010  */1111 1212 import { ACCENT_COLORS, type PasswordPolicy, type SavedIdentity } from "../shared/types";13+import { detectHomograph, getSecurityOriginDisplay } from "../shared/domain";1314 import {1415   DEFAULT_PASSPHRASE_WORDS,1516   generatePassphrase,@@ -59,6 +60,8 @@5960 6061 interface SelectorOptions {6162   domain?: string;63+  /** Full browser origin shown for security decisions; never used for matching. */64+  securityOrigin?: string;6265   /** Browser-issued one-shot authorization for a user-summoned cross-origin selection. */6366   capability?: string;6467   searchableEntries?: SelectorEntry[];@@ -162,7 +165,7 @@162165  * entry's `additionalUrls` so future visits auto-suggest it. Closes after 9s.163166  */164167 export function showRememberSiteToast(165-  pageDomain: string,168+  pageOrigin: string,166169   entryId: string,167170 ): void {168171   const host = document.createElement("div");@@ -195,7 +198,9 @@195198 196199   const bar = document.createElement("div"); bar.className = "bar";197200   const txt = document.createElement("span"); txt.className = "txt";198-  txt.textContent = `Remember ${pageDomain} for this entry?`;201+  const origin = getSecurityOriginDisplay(pageOrigin);202+  const warning = detectHomograph(origin) ? "⚠ Look-alike domain: " : "";203+  txt.textContent = `${warning}Remember ${origin} for this entry?`;199204   const yesBtn = document.createElement("button"); yesBtn.className = "yes";200205   yesBtn.textContent = "Remember";201206   yesBtn.addEventListener("click", async () => {@@ -1518,6 +1523,8 @@15181523   onSelect: (id: string) => void,15191524   options: SelectorOptions = {}15201525 ): () => void {1526+  const securityOrigin = getSecurityOriginDisplay(options.securityOrigin ?? location.origin);1527+  const securityOriginLabel = detectHomograph(securityOrigin) ? `⚠ ${securityOrigin}` : securityOrigin;15211528   const host = document.createElement("div");15221529   // When anchorEl is inside a focus-trapping modal the modal's JS focus trap fires15231530   // when Peach's internal inputs gain focus, immediately blurring them and closing@@ -1731,7 +1738,7 @@17311738     eyebrow.textContent = "NEW ACCOUNT";17321739     const domName = headText.appendChild(document.createElement("div"));17331740     domName.className = "domain-name";1734-    domName.textContent = options.domain ?? location.hostname.replace(/^www\./, "");1741+    domName.textContent = securityOriginLabel;17351742 17361743     const actions = hdr.appendChild(document.createElement("div"));17371744     actions.className = "identity-mode-toggle";@@ -1922,7 +1929,7 @@19221929     eyebrow2.textContent = "NEW ACCOUNT";19231930     const domName2 = hdrRow.appendChild(document.createElement("div"));19241931     domName2.className = "domain-name";1925-    domName2.textContent = options.domain ?? location.hostname.replace(/^www\./, "");1932+    domName2.textContent = securityOriginLabel;19261933 19271934     // Confirmed first step19281935     const conf = wrap.appendChild(document.createElement("div"));@@ -2027,7 +2034,7 @@20272034     eyebrow2.textContent = "NEW ACCOUNT";20282035     const domName2 = hdrRow.appendChild(document.createElement("div"));20292036     domName2.className = "domain-name";2030-    domName2.textContent = options.domain ?? location.hostname.replace(/^www\./, "");2037+    domName2.textContent = securityOriginLabel;20312038 20322039     // Confirmed identity rows — show both username and email when both were collected20332040     function confirmedRow(value: string) {@@ -2086,7 +2093,7 @@20862093 20872094   function renderSaveOffer(filledEmail: string, filledPassword: string, filledUsername?: string) {20882095     container.innerHTML = "";2089-    const domain = options.domain ?? location.hostname.replace(/^www\./, "");2096+    const domain = securityOriginLabel;20902097 20912098     const wrap = container.appendChild(document.createElement("div"));20922099     wrap.className = "save-offer";@@ -2173,7 +2180,7 @@21732180       : entries.length > 0 ? "Saved for this site" : "No saved login";21742181     const domName = document.createElement("div");21752182     domName.className = "domain-name";2176-    domName.textContent = options.domain ?? location.hostname.replace(/^www\./, "");2183+    domName.textContent = securityOriginLabel;21772184     headText.append(eyebrow, domName);21782185     domHdr.appendChild(headText);21792186 @@ -2654,7 +2661,7 @@26542661     eyebrow.textContent = "Saved for this site";26552662     const domName = document.createElement("div");26562663     domName.className = "domain-name";2657-    domName.textContent = options.domain ?? location.hostname.replace(/^www\./, "");2664+    domName.textContent = securityOriginLabel;26582665     headText.append(eyebrow, domName);26592666     domHdr.appendChild(headText);26602667     if (!options.isSignup) {@@ -3137,7 +3144,7 @@31373144     eyebrow.textContent = "vault locked";31383145     const domName = document.createElement("div");31393146     domName.className = "domain-name";3140-    domName.textContent = options.domain ?? location.hostname.replace(/^www\./, "");3147+    domName.textContent = securityOriginLabel;31413148     domHdr.append(eyebrow, domName);31423149     container.appendChild(domHdr);31433150 

view file

src/popup/App.tsx+4 −4
@@ -27,7 +27,7 @@2727 import { AddView } from "./views/Add";2828 import { PeachWordmark } from "./components/PeachMark";2929 import { PasskeyApprovalView } from "./views/PasskeyApproval";30-import { extractDomain } from "../shared/domain";30+import { detectHomograph, extractDomain, getSecurityOriginDisplay } from "../shared/domain";3131 import {3232   syncIndicator,3333   syncIndicatorLabel,@@ -1552,11 +1552,11 @@15521552 15531553             <Show when={pendingRememberSite()}>15541554               {(request) => {1555-                let host = request().url;1556-                try { host = new URL(request().url).hostname; } catch { /* keep raw */ }1555+                const origin = getSecurityOriginDisplay(request().url);1556+                const originLabel = detectHomograph(origin) ? `⚠ ${origin}` : origin;15571557                 return (15581558                   <div class="peach-pro-notice-banner">1559-                    <span>Also use “{request().title}” on {host}?</span>1559+                    <span>Also use “{request().title}” on {originLabel}?</span>15601560                     <button15611561                       class="peach-pro-notice-link"15621562                       onClick={async () => {

view file

src/shared/domain.homograph.test.ts+12 −1
@@ -1,6 +1,6 @@11 // LV-2: homograph detector tests.22 import { describe, expect, it } from "vitest";3-import { detectHomograph } from "./domain";3+import { detectHomograph, getSecurityOriginDisplay } from "./domain";44 55 describe("detectHomograph", () => {66   it("flags a hostname with non-ASCII (Cyrillic homograph) characters", () => {@@ -23,5 +23,16 @@2323   it("returns false for malformed input", () => {2424     expect(detectHomograph("")).toBe(false);2525     expect(detectHomograph("http://")).toBe(false);26+  });27+});28+29+describe("getSecurityOriginDisplay", () => {30+  it("PB-26-707/PB-26-710 preserves scheme, full hostname, and non-default port", () => {31+    expect(getSecurityOriginDisplay("https://login.accounts.example.com:8443/sign-in?next=/vault"))32+      .toBe("https://login.accounts.example.com:8443");33+  });34+35+  it("keeps invalid input visible instead of inventing a trusted origin", () => {36+    expect(getSecurityOriginDisplay("not a URL")).toBe("not a URL");2637   });2738 });

view file

src/shared/domain.ts+12 −0
@@ -28,6 +28,18 @@2828 }2929 3030 /**31+ * Preserve the browser security boundary users need to inspect before a32+ * credential or trust decision: scheme, complete hostname, and port.33+ */34+export function getSecurityOriginDisplay(url: string): string {35+  try {36+    return new URL(url).origin;37+  } catch {38+    return url;39+  }40+}41+42+/**3143  * LV-2: true when the URL's hostname contains non-ASCII characters — a3244  * potential homograph (Cyrillic "аpple.com" etc.) — OR a punycode (xn--)3345  * label, which is the URL parser's normalized form of the same risk.

view file