32a6e4e07185
bind manual summon recall to attested sender domain with bounded payload validation (PB-26-401, v5.0.102)
- time
- 2026-09-05 03:36 UTC · 1 month ago
- machine
- koh2
- agent
- claude-sonnet-4-6
- session
- summon domain validation
- parent
- 5b4ed4f90c88
↓ peach-browser-32a6e4e0.tar.gzatlas at this save
files changed5 files
manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/background/messages.test.ts+26 −0src/background/messages.ts+14 −1manifest.firefox.json+1 −1
@@ -1,7 +1,7 @@11 {22 "manifest_version": 3,33 "name": "Peach Password Manager",4- "version": "5.0.101",4+ "version": "5.0.102",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.101",5+ "version": "5.0.102",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.101",3+ "version": "5.0.102",44 "description": "Zero-knowledge password manager",55 "license": "GPL-3.0-only",66 "type": "module",src/background/messages.test.ts+26 −0
@@ -198,6 +198,32 @@198198 });199199 });200200 201+ it("RECORD_MANUAL_SUMMON binds and bounds the sender domain", async () => {202+ const sender = {203+ id: "peach-extension",204+ url: "https://login.example.com/form",205+ origin: "https://login.example.com",206+ tab: { id: 1 },207+ frameId: 0,208+ } as chrome.runtime.MessageSender;209+210+ await expect(dispatch({211+ type: "RECORD_MANUAL_SUMMON",212+ payload: { domain: "evil.example" },213+ }, sender)).resolves.toEqual({ ok: false, error: "invalid_domain" });214+ await expect(dispatch({215+ type: "RECORD_MANUAL_SUMMON",216+ payload: { domain: "example.com" },217+ }, sender)).resolves.toEqual({ ok: true });218+ await expect(dispatch({219+ type: "RECORD_MANUAL_SUMMON",220+ payload: { domain: "a".repeat(254) },221+ }, { ...sender, origin: "https://a.com", url: "https://a.com/form" })).resolves.toEqual({222+ ok: false,223+ error: "invalid_domain",224+ });225+ });226+201227 it("GET_ENTRIES when unlocked returns the entry array", async () => {202228 const vault = await import("./vault");203229 await vault.createVault("correct-pass-12345");src/background/messages.ts+14 −1
@@ -1329,7 +1329,20 @@13291329 }13301330 }13311331 case "RECORD_MANUAL_SUMMON": {1332- const { domain } = msg.payload as { domain: string };1332+ const { domain } = (msg.payload ?? {}) as { domain?: unknown };1333+ const origin = attestedSenderOrigin(sender);1334+ let senderDomain: string | null = null;1335+ if (origin) {1336+ try {1337+ const hostname = new URL(origin).hostname;1338+ senderDomain = extractDomain(hostname) ?? hostname;1339+ } catch {1340+ senderDomain = null;1341+ }1342+ }1343+ if (typeof domain !== "string" || domain.length > 253 || domain !== senderDomain) {1344+ return { ok: false, error: "invalid_domain" };1345+ }13331346 const settings = vault.getSettings();13341347 const existing = settings.manualSummonDomains?.[domain];13351348 const now = Date.now();