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
↓ peach-browser-bdcc99ca.tar.gzatlas at this save
files changed9 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 −0manifest.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": {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",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",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", () => {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 },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 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 () => {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 });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.