2ee6e8b00769
fix: card autofill applies the visibility gate and form-action check requires https on-domain-or-subdomain (PB-26-007, PB-26-008) — hidden card fields no longer filled, crafted forms on sibling registrable domains or plain http refused; TDD regression tests; bump 5.0.7→5.0.8
- time
- 2026-08-30 00:12 UTC · 1 month ago
- machine
- koh2
- agent
- glm-5.3-flash
- session
- pb-26 audit fixes
- parent
- a838d84d8945
↓ peach-browser-2ee6e8b0.tar.gzatlas at this save
files changed5 files
manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/content/fill.test.ts+83 −0src/content/fill.ts+15 −8manifest.firefox.json+1 −1
@@ -1,7 +1,7 @@11 {22 "manifest_version": 3,33 "name": "Peach Password Manager",4- "version": "5.0.7",4+ "version": "5.0.8",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.7",5+ "version": "5.0.8",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.7",3+ "version": "5.0.8",44 "description": "Zero-knowledge password manager",55 "license": "GPL-3.0-only",66 "type": "module",src/content/fill.test.ts+83 −0
@@ -54,6 +54,36 @@5454 expect(password.value).toBe("");5555 });5656 57+ // PB-26-008: where a <form> exists, the action must be https and on the58+ // expected domain or a subdomain of it — not merely the same eTLD+1.59+ it("fillFields refuses a plain-http form action (PB-26-008)", () => {60+ const form = document.createElement("form");61+ form.action = "http://github.com/login";62+ const username = makeVisibleInput("text");63+ const password = makeVisibleInput("password");64+ form.append(username, password);65+ document.body.appendChild(form);66+67+ fillFields(username, password, "june", "hunter2", form, "github.com");68+69+ expect(username.value).toBe("");70+ expect(password.value).toBe("");71+ });72+73+ it("fillFields allows an https form action on a subdomain of the expected domain (PB-26-008)", () => {74+ const form = document.createElement("form");75+ form.action = "https://auth.github.com/session";76+ const username = makeVisibleInput("text");77+ const password = makeVisibleInput("password");78+ form.append(username, password);79+ document.body.appendChild(form);80+81+ fillFields(username, password, "june", "hunter2", form, "github.com");82+83+ expect(username.value).toBe("june");84+ expect(password.value).toBe("hunter2");85+ });86+5787 it("fillFields still fills the username when the username field is hidden", () => {5888 const username = makeVisibleInput("text");5989 const password = makeVisibleInput("password");@@ -99,9 +129,62 @@99129 function makeInput(attrs: Record<string, string> = {}): HTMLInputElement {100130 const input = document.createElement("input");101131 for (const [k, v] of Object.entries(attrs)) input.setAttribute(k, v);132+ // jsdom rects are all-zero, which isVisible treats as hidden — stub a133+ // real rect so these inputs count as visible unless a test hides them.134+ Object.defineProperty(input, "getBoundingClientRect", {135+ configurable: true,136+ value: () => ({137+ width: 100,138+ height: 20,139+ top: 0,140+ left: 0,141+ right: 100,142+ bottom: 20,143+ x: 0,144+ y: 0,145+ toJSON: () => ({}),146+ }),147+ });102148 document.body.appendChild(input);103149 return input;104150 }151+152+ function makeHiddenInput(attrs: Record<string, string> = {}): HTMLInputElement {153+ const input = makeInput(attrs);154+ Object.defineProperty(input, "getBoundingClientRect", {155+ configurable: true,156+ value: () => ({157+ width: 0,158+ height: 0,159+ top: 0,160+ left: 0,161+ right: 0,162+ bottom: 0,163+ x: 0,164+ y: 0,165+ toJSON: () => ({}),166+ }),167+ });168+ return input;169+ }170+171+ // PB-26-007: card fill must apply the same visibility gate as the172+ // password path — a crafted page must not be able to exfiltrate card173+ // data into hidden fields.174+ it("fillCardForm — skips hidden fields (PB-26-007)", () => {175+ const number = makeHiddenInput({ type: "text" });176+ const cvc = makeHiddenInput({ type: "text" });177+ const name = makeInput({ type: "text" });178+179+ fillCardForm(180+ { number, expMonth: null, expYear: null, expiryCombo: null, cvc, name },181+ { cardNumber: "4111111111111111", expMonth: "05", expYear: "2028", cvc: "123", cardholderName: "Jane Smith" }182+ );183+184+ expect(number.value).toBe("");185+ expect(cvc.value).toBe("");186+ expect(name.value).toBe("Jane Smith");187+ });105188 106189 it("fillCardForm — fills number, expiry, CVC, and name fields", () => {107190 const number = makeInput({ type: "text" });src/content/fill.ts+15 −8
@@ -9,8 +9,13 @@99 if (!form?.action) return true;1010 try {1111 const actionUrl = new URL(form.action);12- const actionDomain = getRegistrableDomain(actionUrl.hostname);13- if (actionDomain !== expectedDomain) {12+ // PB-26-008: a <form> element is a strong origin signal, so require13+ // https and a host on the expected domain or a subdomain of it — not14+ // merely any site sharing the registrable domain (eTLD+1).15+ if (actionUrl.protocol !== "https:") return false;16+ const host = actionUrl.hostname;17+ if (getRegistrableDomain(host) !== expectedDomain) return false;18+ if (host !== expectedDomain && !host.endsWith(`.${expectedDomain}`)) {1419 return false;1520 }1621 } catch {}@@ -93,16 +98,18 @@9398 }9499 95100 export function fillCardForm(fields: CardFillFields, card: { cardNumber: string; expMonth: string; expYear: string; cvc: string; cardholderName: string }): void {101+ // PB-26-007: same visibility gate as the password/email path in102+ // fillFields — a crafted page must not feed card data into hidden fields.96103 const raw = card.cardNumber.replace(/\s/g, '');97- if (fields.number) setNativeValue(fields.number, formatCardNumber(raw));98- if (fields.expiryCombo) {104+ if (fields.number && isVisible(fields.number)) setNativeValue(fields.number, formatCardNumber(raw));105+ if (fields.expiryCombo && isVisible(fields.expiryCombo)) {99106 setNativeValue(fields.expiryCombo, `${card.expMonth}/${card.expYear.slice(-2)}`);100107 } else {101- if (fields.expMonth) setNativeValue(fields.expMonth, card.expMonth);102- if (fields.expYear) setNativeValue(fields.expYear, card.expYear);108+ if (fields.expMonth && isVisible(fields.expMonth)) setNativeValue(fields.expMonth, card.expMonth);109+ if (fields.expYear && isVisible(fields.expYear)) setNativeValue(fields.expYear, card.expYear);103110 }104- if (fields.cvc) setNativeValue(fields.cvc, card.cvc);105- if (fields.name) setNativeValue(fields.name, card.cardholderName);111+ if (fields.cvc && isVisible(fields.cvc)) setNativeValue(fields.cvc, card.cvc);112+ if (fields.name && isVisible(fields.name)) setNativeValue(fields.name, card.cardholderName);106113 }107114 108115 export function formatCardNumber(raw: string): string {