af297f42b5d9

bind passkey approval responses to originating tab and origin (v5.0.69)

time
2026-09-03 21:38 UTC · 1 month ago
machine
koh2
agent
claude-sonnet-4-6
session
PB-26-301 passkey approval binding
parent
2c4fb1e1da46
7 files manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/background/messages.test.ts+19 −0src/background/messages.ts+9 −0src/background/passkey.ts+8 −0src/shared/types.ts+3 −0
manifest.firefox.json+1 −1
@@ -1,7 +1,7 @@11 {22   "manifest_version": 3,33   "name": "Peach Password Manager",4-  "version": "5.0.68",4+  "version": "5.0.69",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.68",5+  "version": "5.0.69",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.68",3+  "version": "5.0.69",44   "description": "Zero-knowledge password manager",55   "license": "GPL-3.0-only",66   "type": "module",

view file

src/background/messages.test.ts+19 −0
@@ -613,6 +613,25 @@613613     breachSpy.mockRestore();614614   });615615 616+  it("rejects passkey approval from a different tab than the ceremony", async () => {617+    sessionStorage.passkey_pending_301 = {618+      requestId: 301,619+      type: "get",620+      rpId: "example.com",621+      rpName: "Example",622+      userName: "[email protected]",623+      tabId: 7,624+      origin: "https://example.com",625+    };626+627+    await expect(628+      dispatch(629+        { type: "PASSKEY_AUTH_RESPONSE", payload: { requestId: 301, approved: true } },630+        { id: "peach-extension", tab: { id: 8 }, url: "https://example.com/login" } as chrome.runtime.MessageSender,631+      ),632+    ).resolves.toMatchObject({ ok: false, error: "invalid_passkey_approval_sender" });633+  });634+616635   it("messages from foreign or URL-less senders are rejected", async () => {617636     const vault = await import("./vault");618637     await vault.createVault("correct-pass-12345");

view file

src/background/messages.ts+9 −0
@@ -1404,6 +1404,15 @@14041404         if (requestId === undefined || requestId === null || typeof approved !== 'boolean') {14051405           return { ok: false, error: 'requestId and approved required' };14061406         }1407+        const pending = await getPendingPasskeyRequest();1408+        const senderOrigin = attestedSenderOrigin(sender);1409+        if (1410+          (pending && pending.requestId !== Number(requestId)) ||1411+          (pending?.tabId !== undefined && sender.tab?.id !== pending.tabId) ||1412+          (pending?.origin !== undefined && senderOrigin !== pending.origin)1413+        ) {1414+          return { ok: false, error: "invalid_passkey_approval_sender" };1415+        }14071416         await resolvePendingPasskeyRequest(Number(requestId), approved);14081417         return { ok: true };14091418       }

view file

src/background/passkey.ts+8 −0
@@ -722,6 +722,13 @@722722     return;723723   }724724 725+  const tabs = await new Promise<chrome.tabs.Tab[]>((resolve) => {726+    chrome.tabs.query({ active: true, currentWindow: true }, resolve);727+  });728+  const approvalBinding = {729+    tabId: tabs[0]?.id,730+    origin: await resolvePageOrigin(credential.rpId),731+  };725732   const approved = await promptViaToast({726733     requestId: request.requestId,727734     type: "get",@@ -729,6 +736,7 @@729736     rpName: credential.rpName,730737     userName: credential.userName,731738     userDisplayName: credential.userDisplayName,739+    ...approvalBinding,732740   });733741   if (!approved) {734742     await rejectGet(request.requestId, "NotAllowedError", "User denied request.");

view file

src/shared/types.ts+3 −0
@@ -146,6 +146,9 @@146146   rpName: string;147147   userName: string;148148   userDisplayName?: string;149+  /** Originating tab and frame binding for content-script approval responses. */150+  tabId?: number;151+  origin?: string;149152   /**150153    * Firefox path: when present, the popup performs the navigator.credentials151154    * call itself rather than completing a chrome.webAuthenticationProxy

view file