d48cf50b1d35

propagate form-action gate to TOTP fill and canary (PB-26-105, v5.0.36)

time
2026-09-01 21:05 UTC · 1 month ago
machine
koh2
agent
gpt-5
session
2026-09-01 security assessment remediation
parent
5c0a970d298e
6 files manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/content/fill.test.ts+27 −1src/content/fill.ts+5 −2src/content/index.ts+12 −11
manifest.firefox.json+1 −1
@@ -1,7 +1,7 @@11 {22   "manifest_version": 3,33   "name": "Peach Password Manager",4-  "version": "5.0.35",4+  "version": "5.0.36",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.35",5+  "version": "5.0.36",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.35",3+  "version": "5.0.36",44   "description": "Zero-knowledge password manager",55   "license": "GPL-3.0-only",66   "type": "module",

view file

src/content/fill.test.ts+27 −1
@@ -2,7 +2,7 @@22 33 import { beforeEach, describe, expect, it, vi } from "vitest";44 5-import { fillFields, fillCardForm, formatCardNumber } from "./fill";5+import { fillFields, fillCardForm, fillTOTPCode, formatCardNumber } from "./fill";66 77 describe("content fill", () => {88   beforeEach(() => {@@ -105,6 +105,32 @@105105 106106     expect(username.value).toBe("june");107107     expect(password.value).toBe("hunter2");108+  });109+110+  it("fillTOTPCode respects the form-action gate (PB-26-105)", () => {111+    const form = document.createElement("form");112+    form.action = "https://evil.com/login";113+    const totpEl = makeVisibleInput("text");114+    form.appendChild(totpEl);115+    document.body.appendChild(form);116+117+    const filled = fillTOTPCode(totpEl, "123456", form, "github.com");118+119+    expect(filled).toBe(false);120+    expect(totpEl.value).toBe("");121+  });122+123+  it("fillTOTPCode allows https form action on expected domain (PB-26-105)", () => {124+    const form = document.createElement("form");125+    form.action = "https://github.com/login";126+    const totpEl = makeVisibleInput("text");127+    form.appendChild(totpEl);128+    document.body.appendChild(form);129+130+    const filled = fillTOTPCode(totpEl, "123456", form, "github.com");131+132+    expect(filled).toBe(true);133+    expect(totpEl.value).toBe("123456");108134   });109135 110136   it("fillFields dispatches standard input/change events rather than custom password events", () => {

view file

src/content/fill.ts+5 −2
@@ -5,7 +5,7 @@55   return extractDomain(hostname) ?? hostname;66 }77 8-function checkFormAction(form: HTMLFormElement | null, expectedDomain: string): boolean {8+export function checkFormAction(form: HTMLFormElement | null, expectedDomain: string): boolean {99   if (!form?.action) return true;1010   try {1111     const actionUrl = new URL(form.action);@@ -61,10 +61,13 @@6161 6262 export function fillTOTPCode(6363   totpEl: HTMLInputElement | null,64-  code: string64+  code: string,65+  form?: HTMLFormElement | null,66+  expectedDomain?: string6567 ): boolean {6668   if (!totpEl || !code || !isVisible(totpEl)) return false;6769   if (totpEl.value.trim() !== "") return false;70+  if (expectedDomain && form && !checkFormAction(form, expectedDomain)) return false;6871   setNativeValue(totpEl, code);6972   return true;7073 }

view file

src/content/index.ts+12 −11
@@ -27,7 +27,7 @@2727 } from "./inject";2828 import { detectBrowser } from "../shared/browser";2929 import { FIREFOX_USE_CONTENT_SCRIPT_PROXY } from "../shared/passkey-flags";30-import { fillCardForm, fillFields, fillOTPDigitBoxes, fillTOTPCode, setNativeValue } from "./fill";30+import { fillCardForm, fillFields, fillOTPDigitBoxes, fillTOTPCode, setNativeValue, checkFormAction } from "./fill";3131 import { resolveAutofillInteractionTargets } from "./interaction";3232 import { setupSaveDetection } from "./save";3333 import { contentMLDetector } from "./content-ml-detector";@@ -314,8 +314,8 @@314314   passwordEl: HTMLInputElement | null,315315   entry: VaultEntry,316316   emailEl?: HTMLInputElement | null317-): Promise<HTMLInputElement | null> {318-  if (!entry.username && !entry.password && !entry.email) return null;317+): Promise<{ filledTOTPField: HTMLInputElement | null; gatePassed: boolean }> {318+  if (!entry.username && !entry.password && !entry.email) return { filledTOTPField: null, gatePassed: false };319319   const pageDomain = getPageDomain();320320   const form = (passwordEl ?? usernameEl)?.closest("form") as HTMLFormElement | null ?? null;321321   // Fill the email field with entry.email if set; fall back to entry.username when@@ -323,9 +323,10 @@323323   // separate email field on signup forms).324324   const emailValue = entry.email ?? (entry.username?.includes("@") ? entry.username : "");325325   fillFields(usernameEl, passwordEl, entry.username ?? "", entry.password ?? "", form, pageDomain, emailEl, emailValue);326+  const gatePassed = !form || !pageDomain || checkFormAction(form, pageDomain);326327   let filledTOTPField: HTMLInputElement | null = null;327328 328-  if (entry.canary) {329+  if (entry.canary && gatePassed) {329330     chrome.runtime.sendMessage({330331       type: "FIRE_CANARY_PING",331332       payload: {@@ -352,10 +353,10 @@352353   // The background sends a freshly-generated 6-digit code (totpCode), never the353354   // base32 seed (SEV-3-C). Fill the code straight in.354355   const totpCode = (entry as { totpCode?: string }).totpCode;355-  if (totpCode) {356+  if (totpCode && gatePassed) {356357     const code = totpCode;357358     const totpField = passwordEl ? findSameLoginTOTPField(passwordEl) : null;358-    if (fillTOTPCode(totpField, code)) {359+    if (fillTOTPCode(totpField, code, form, pageDomain)) {359360       filledTOTPField = totpField;360361     }361362     let alreadyWritten = false;@@ -383,7 +384,7 @@383384     }384385   }385386 386-  return filledTOTPField;387+  return { filledTOTPField, gatePassed };387388 }388389 389390 async function fetchAutofillEntries(@@ -482,10 +483,10 @@482483         type: "GET_ENTRY_FOR_FILL",483484         payload: { id },484485       });485-      const entry = (entryResponse as { ok: boolean; data?: VaultEntry }).data;486-      if (entry) {487-        const filledTOTPField = await handleAutofill(anchorField, usernameEl, passwordEl, entry, emailEl);488-        recordFillSuccess(anchorField, usernameEl, passwordEl, filledTOTPField);486+       const entry = (entryResponse as { ok: boolean; data?: VaultEntry }).data;487+       if (entry) {488+         const { filledTOTPField, gatePassed } = await handleAutofill(anchorField, usernameEl, passwordEl, entry, emailEl);489+         if (gatePassed) recordFillSuccess(anchorField, usernameEl, passwordEl, filledTOTPField);489490         // Offer to remember the current site for this entry only if none of490491         // its known domains (primary + additional) match — otherwise the491492         // user already mapped it and the prompt would be noise.

view file