d7d47c3cc1d2

PB-26-201 discriminating authorization tests pass with v5.0.54

time
2026-09-02 12:49 UTC · 1 month ago
machine
koh2
agent
claude-sonnet-4-6
session
PB-26-201 authorization
parent
151df7df811c
3 files src/background/messages.adversarial.test.ts+10 −10src/background/messages.edit-origin.test.ts+15 −14src/background/messages.test.ts+4 −4
src/background/messages.adversarial.test.ts+10 −10
@@ -628,42 +628,42 @@628628       });629629     }630630 631-    it("tab-context request on the entry's domain returns the full entry", async () => {631+    it("PB-26-201: tab-context GET_ENTRY_FOR_EDIT is denied", async () => {632632       const e = await addGithubEntry();633633       const r = await dispatchFrom(634634         { type: "GET_ENTRY_FOR_EDIT", payload: { id: e.id } } as Message,635635         { id: "peach-extension", tab: { url: "https://github.com/login" } } as chrome.runtime.MessageSender,636636       );637-      expect(r.ok).toBe(true);638-      expect((r as { data: Record<string, unknown> }).data.username).toBe("june");637+      expect(r.ok).toBe(false);638+      expect(r.error).toBe("invalid_sender");639639     });640640 641-    it("tab-context request on a mismatched domain returns entry_not_found with no leak", async () => {641+    it("PB-26-201: foreign tab cannot query edit data", async () => {642642       const e = await addGithubEntry();643643       const r = await dispatchFrom(644644         { type: "GET_ENTRY_FOR_EDIT", payload: { id: e.id } } as Message,645645         { id: "peach-extension", tab: { url: "https://evil.example/login" } } as chrome.runtime.MessageSender,646646       );647647       expect(r.ok).toBe(false);648-      expect(r.error).toBe("entry_not_found");648+      expect(r.error).toBe("invalid_sender");649649       expect((r as { data?: unknown }).data).toBeUndefined();650650     });651651 652-    it("explicitSummon allows a cross-domain tab-context request", async () => {652+    it("PB-26-201: explicitSummon cannot authorize edit data", async () => {653653       const e = await addGithubEntry();654654       const r = await dispatchFrom(655655         { type: "GET_ENTRY_FOR_EDIT", payload: { id: e.id, explicitSummon: true } } as Message,656656         { id: "peach-extension", tab: { url: "https://evil.example/login" } } as chrome.runtime.MessageSender,657657       );658-      expect(r.ok).toBe(true);659-      expect((r as { data: Record<string, unknown> }).data.username).toBe("june");658+      expect(r.ok).toBe(false);659+      expect(r.error).toBe("invalid_sender");660660     });661661 662-    it("popup context (no tab/url) skips the origin check and returns the entry", async () => {662+    it("PB-26-201: extension popup remains authorized for edit data", async () => {663663       const e = await addGithubEntry();664664       const r = await dispatchFrom(665665         { type: "GET_ENTRY_FOR_EDIT", payload: { id: e.id } } as Message,666-        { id: "peach-extension" } as chrome.runtime.MessageSender,666+        { id: "peach-extension", url: "chrome-extension://peach-extension/src/popup/index.html" } as chrome.runtime.MessageSender,667667       );668668       expect(r.ok).toBe(true);669669       expect((r as { data: Record<string, unknown> }).data.username).toBe("june");

view file

src/background/messages.edit-origin.test.ts+15 −14
@@ -129,7 +129,7 @@129129 130130     const { ok, data } = result as { ok: boolean; data: unknown[] };131131     expect(ok).toBe(true);132-    // includeAll was stripped; only domain-matched entries are returned132+    // Only domain-matched entries are returned133133     // "Other Site" (other.example.com) must not appear134134     const titles = (data as Array<{ title: string }>).map((e) => e.title);135135     expect(titles).not.toContain("Other Site");@@ -143,7 +143,7 @@143143   //144144   // Bug this catches: removing the senderDomain check in GET_ENTRY_FOR_EDIT145145   // allows any tab-context sender to enumerate entry details by guessing IDs.146-  it("T-022c: GET_ENTRY_FOR_EDIT from wrong-domain sender.url → entry_not_found", async () => {146+  it("PB-26-201: GET_ENTRY_FOR_EDIT is unreachable from tab senders", async () => {147147     const entry = await vault.addEntry({148148       type: "login",149149       title: "Target Entry",@@ -167,13 +167,13 @@167167     );168168 169169     expect((result as { ok: boolean; error: string }).ok).toBe(false);170-    expect((result as { ok: boolean; error: string }).error).toBe("entry_not_found");170+    expect((result as { ok: boolean; error: string }).error).toBe("invalid_sender");171171   });172172 173173   // T-022d: GET_ENTRY_FOR_EDIT with matching sender domain → succeeds.174174   // The control case for T-022c: a sender from the same domain as the entry175175   // must be allowed through.176-  it("T-022d: GET_ENTRY_FOR_EDIT from matching sender.url → returns entry", async () => {176+  it("PB-26-201: matching tab sender cannot call GET_ENTRY_FOR_EDIT", async () => {177177     const entry = await vault.addEntry({178178       type: "login",179179       title: "Same Domain Entry",@@ -195,14 +195,14 @@195195       matchingSender,196196     );197197 198-    const { ok, data } = result as { ok: boolean; data: { title: string } };199-    expect(ok).toBe(true);200-    expect(data.title).toBe("Same Domain Entry");198+    const { ok } = result as { ok: boolean };199+    expect(ok).toBe(false);200+    expect((result as { error?: string }).error).toBe("invalid_sender");201201   });202202 203203   // PB-26-005: the frame url (sender.url) is authoritative over the top-frame204204   // sender.tab.url for an all_frames content script.205-  it("PB-26-005: iframe sender.url matching the entry wins over a non-matching top-frame tab.url", async () => {205+  it("PB-26-201: iframe tab sender cannot call GET_ENTRY_FOR_EDIT", async () => {206206     const entry = await vault.addEntry({207207       type: "login",208208       title: "Frame Entry",@@ -227,7 +227,8 @@227227     );228228 229229     const { ok } = result as { ok: boolean };230-    expect(ok).toBe(true);230+    expect(ok).toBe(false);231+    expect((result as { error?: string }).error).toBe("invalid_sender");231232   });232233 233234   it("PB-26-005: attacker iframe sender.url denies even when the top frame matches the entry", async () => {@@ -253,14 +254,14 @@253254     );254255 255256     expect((result as { ok: boolean; error?: string }).ok).toBe(false);256-    expect((result as { error?: string }).error).toBe("entry_not_found");257+    expect((result as { error?: string }).error).toBe("invalid_sender");257258   });258259 259260   // T-022e: GET_ENTRY_FOR_EDIT with explicitSummon=true bypasses the domain check.260261   // The explicit summon flag is set only by user-initiated keyboard shortcuts,261262   // never by passive autofill. It allows the edit form to open for entries on262263   // a subdomain (e.g., accounts.google.com for a google.com entry).263-  it("T-022e: GET_ENTRY_FOR_EDIT with explicitSummon=true from cross-domain sender → returns entry", async () => {264+  it("PB-26-201: explicitSummon cannot authorize GET_ENTRY_FOR_EDIT", async () => {264265     const entry = await vault.addEntry({265266       type: "login",266267       title: "Explicit Summon Entry",@@ -282,8 +283,8 @@282283       crossDomainSender,283284     );284285 285-    const { ok, data } = result as { ok: boolean; data: { title: string } };286-    expect(ok).toBe(true);287-    expect(data.title).toBe("Explicit Summon Entry");286+    const { ok } = result as { ok: boolean };287+    expect(ok).toBe(false);288+    expect((result as { error?: string }).error).toBe("invalid_sender");288289   });289290 });

view file

src/background/messages.test.ts+4 −4
@@ -997,7 +997,7 @@997997     expect(res).toMatchObject({ ok: false, error: "entry_not_found" });998998   });999999 1000-  it("MV-1: mismatched domain with explicitSummon:true returns the entry (user-chosen off-domain fill)", async () => {1000+  it("PB-26-201: explicitSummon cannot authorize off-domain fill", async () => {10011001     const vault = await import("./vault");10021002     await vault.createVault("correct-pass-12345");10031003     const entry = await vault.addEntry({@@ -1014,7 +1014,7 @@10141014       },10151015       tabSender("https://evil.example.com/phish"),10161016     );1017-    expect(res).toMatchObject({ ok: true, data: { id: entry.id } });1017+    expect(res).toMatchObject({ ok: false, error: "entry_not_found" });10181018   });10191019 10201020   it("MV-1: tab context with an unparseable URL is refused as origin_unverified", async () => {@@ -1067,7 +1067,7 @@10671067     expect(data[0].title).toBe("GitHub");10681068   });10691069 1070-  it("MV-2: AUTOFILL_QUERY with includeAll + userGesture returns the vault-wide list", async () => {1070+  it("PB-26-201: userGesture cannot authorize vault-wide inventory", async () => {10711071     await seedTwoDomainVault();10721072 10731073     const res = await dispatch({@@ -1076,7 +1076,7 @@10761076     });10771077     expect(res.ok).toBe(true);10781078     const data = res.data as Array<{ title: string }>;1079-    expect(data.map((d) => d.title).sort()).toEqual(["Example", "GitHub"]);1079+    expect(data.map((d) => d.title)).toEqual(["GitHub"]);10801080   });10811081 10821082   // --- MV-7: vouched device confirmation handlers ---

view file