07bf0e84d1db

fix popup port authentication to require extension-page sender origin and reject content scripts (v5.0.73)

time
2026-09-03 22:19 UTC · 1 month ago
machine
koh2
agent
claude-sonnet-4-6
session
PB-26-305 popup port authentication
parent
37b22166bab5
5 files manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/background/entry.lock-port.test.ts+21 −5src/background/entry.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.72",4+  "version": "5.0.73",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.72",5+  "version": "5.0.73",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.72",3+  "version": "5.0.73",44   "description": "Zero-knowledge password manager",55   "license": "GPL-3.0-only",66   "type": "module",

view file

src/background/entry.lock-port.test.ts+21 −5
@@ -1,19 +1,21 @@1-// LV-5: the lock-on-close onConnect listener accepts only our nonce'd2-// "popup-<uuid>" port and ignores anything else.1+// The lock-on-close onConnect listener accepts only our nonce'd2+// "popup-<uuid>" port from the extension page and ignores anything else.33 import { beforeEach, describe, expect, it, vi } from "vitest";44 55 type StorageShape = Record<string, unknown>;66 77 interface FakePort {88   name: string;9+  sender?: chrome.runtime.MessageSender;910   onDisconnect: { addListener: (fn: () => void) => void };1011   fireDisconnect: () => Promise<void>;1112 }1213 13-function makePort(name: string): FakePort {14+function makePort(name: string, sender?: chrome.runtime.MessageSender): FakePort {1415   let handler: (() => void) | null = null;1516   return {1617     name,18+    sender,1719     onDisconnect: {1820       addListener: (fn: () => void) => {1921         handler = fn;@@ -45,7 +47,7 @@4547         onMessage: { addListener: vi.fn() },4648         sendMessage: vi.fn(async () => undefined),4749         connect: vi.fn(),48-        getURL: vi.fn((p: string) => p),50+        getURL: vi.fn((p: string) => `chrome-extension://test/${p}`),4951       },5052       storage: {5153         local: {@@ -114,11 +116,25 @@114116     const vault = await setupUnlockedVaultWithLockOnClose();115117     expect(connectListeners).toHaveLength(1);116118 117-    const port = makePort(`popup-${"a1b2c3"}`);119+    const port = makePort(`popup-${"a1b2c3"}`, { url: "chrome-extension://test/src/popup/index.html" });118120     connectListeners[0](port);119121     await port.fireDisconnect();120122 121123     expect(vault.isUnlocked()).toBe(false);124+  });125+126+  it("ignores a nonce'd popup port from a content script", async () => {127+    await import("./entry");128+    const vault = await setupUnlockedVaultWithLockOnClose();129+130+    const port = makePort(`popup-${"a1b2c3"}`, {131+      url: "https://example.com/",132+      tab: { id: 7 } as chrome.tabs.Tab,133+    });134+    connectListeners[0](port);135+    await port.fireDisconnect();136+137+    expect(vault.isUnlocked()).toBe(true);122138   });123139 124140   it.each(["popup", "evil"])("ignores the non-nonce port name %s", async (name) => {

view file

src/background/entry.ts+3 −0
@@ -48,6 +48,9 @@4848 4949 chrome.runtime.onConnect.addListener((port) => {5050   if (!port.name.startsWith(POPUP_PORT_PREFIX)) return;51+  const senderUrl = port.sender?.url;52+  const extensionOrigin = new URL(chrome.runtime.getURL("")).origin;53+  if (port.sender?.tab || !senderUrl || new URL(senderUrl).origin !== extensionOrigin) return;5154   port.onDisconnect.addListener(async () => {5255     await denyAllPendingPasskeyRequests();5356     const s = await ensureSettingsLoaded();

view file