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