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
↓ peach-browser-d7d47c3c.tar.gzatlas at this save
files changed3 files
src/background/messages.adversarial.test.ts+10 −10src/background/messages.edit-origin.test.ts+15 −14src/background/messages.test.ts+4 −4src/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");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 });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 ---