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