/** * security-sentinels.spec.ts — PB-26-210 mandatory Chrome release browser gate. * * Security sentinels that must pass (non-skipped, assertion-bearing) before * any release can report success: * * - PB-26-201: a hostile page cannot obtain vault-wide metadata, a foreign * password, a raw TOTP seed, or password history via content-script message * shapes. Attacker-supplied Booleans (userGesture, explicitSummon) have no * authorization effect. * - PB-26-203: a real content script cannot read peach_session_dk from * chrome.storage.session after unlock — the TRUSTED_CONTEXTS access level * enforced by the storage-hardening initializer blocks content scripts. */ import { test, expect, type BrowserContext } from "@playwright/test"; import { makeContext, setupVault, addEntry } from "./helpers/vault"; test.describe.configure({ timeout: 120_000 }); test.describe("PB-26-201 @security: hostile page cannot obtain foreign secrets", () => { let ctx: BrowserContext; test.beforeEach(async () => { ctx = await makeContext("pb26201-sentinel"); }); test.afterEach(async () => { await ctx.close(); }); test("content script GET_ENTRY_FOR_EDIT is denied (@security)", async () => { const { popup } = await setupVault(ctx); await addEntry(popup, { title: "Foreign Entry", url: "https://example.org/login", username: "foreign@example.org", password: "foreign-secret-totally-private", }); const page = await ctx.newPage(); await page.goto("https://example.com/"); const result = await popup.evaluate(async () => { const tabs = await chrome.tabs.query({ url: "https://example.com/*" }); const tab = tabs.find((t) => t.id !== undefined); if (!tab?.id) throw new Error("test page tab not found"); const exec = await chrome.scripting.executeScript({ target: { tabId: tab.id }, func: async () => { const editResp = await chrome.runtime.sendMessage({ type: "GET_ENTRY_FOR_EDIT", payload: { id: "any-id" }, }); return { editResp }; }, }); return exec[0]?.result as { editResp: { ok?: boolean; error?: string } }; }); expect(result?.editResp?.ok).toBe(false); expect(result?.editResp?.error).toBe("invalid_sender"); }); test("userGesture and explicitSummon Booleans cannot authorize GET_ENTRIES (@security)", async () => { const { popup } = await setupVault(ctx); await addEntry(popup, { title: "Secret Vault Entry", url: "https://example.org/login", username: "secret@example.org", password: "should-never-leak", }); const page = await ctx.newPage(); await page.goto("https://example.com/"); const result = await popup.evaluate(async () => { const tabs = await chrome.tabs.query({ url: "https://example.com/*" }); const tab = tabs.find((t) => t.id !== undefined); if (!tab?.id) throw new Error("test page tab not found"); const exec = await chrome.scripting.executeScript({ target: { tabId: tab.id }, func: async () => { const entriesResp = await chrome.runtime.sendMessage({ type: "GET_ENTRIES", payload: { includeAll: true, userGesture: true, explicitSummon: true }, }); return { entriesResp }; }, }); return exec[0]?.result as { entriesResp: { ok?: boolean; error?: string } }; }); expect(result?.entriesResp?.ok).toBe(false); expect(result?.entriesResp?.error).toBe("invalid_sender"); }); test("AUTOFILL_QUERY on hostile page returns no foreign passwords or TOTP seeds (@security)", async () => { const { popup } = await setupVault(ctx); await addEntry(popup, { title: "Example Org", url: "https://example.org/login", username: "user@example.org", password: "foreign-pass-12345", totp: "JBSWY3DPEHPK3PXP", }); const page = await ctx.newPage(); await page.goto("https://example.com/"); const result = await popup.evaluate(async () => { const tabs = await chrome.tabs.query({ url: "https://example.com/*" }); const tab = tabs.find((t) => t.id !== undefined); if (!tab?.id) throw new Error("test page tab not found"); const exec = await chrome.scripting.executeScript({ target: { tabId: tab.id }, func: async () => { // AUTOFILL_QUERY is content-script-allowed but must be origin-scoped. // example.com should get NO results for example.org entries. const queryResp = await chrome.runtime.sendMessage({ type: "AUTOFILL_QUERY", payload: { explicitSummon: true, userGesture: true }, }); return { queryResp: JSON.stringify(queryResp) }; }, }); return exec[0]?.result as { queryResp: string }; }); // The response must not contain the foreign password, TOTP seed, or // the foreign origin's entry data. expect(result?.queryResp).not.toContain("foreign-pass-12345"); expect(result?.queryResp).not.toContain("JBSWY3DPEHPK3PXP"); expect(result?.queryResp).not.toContain("example.org"); }); }); test.describe("PB-26-203 @security: content script cannot read session storage key", () => { let ctx: BrowserContext; test.beforeEach(async () => { ctx = await makeContext("pb26203-sentinel"); }); test.afterEach(async () => { await ctx.close(); }); test("content script cannot read peach_session_dk from storage.session after unlock (@security)", async () => { const { popup } = await setupVault(ctx); // Navigate to a real page so a content script context exists. const page = await ctx.newPage(); await page.goto("https://example.com/"); const result = await popup.evaluate(async () => { const tabs = await chrome.tabs.query({ url: "https://example.com/*" }); const tab = tabs.find((t) => t.id !== undefined); if (!tab?.id) throw new Error("test page tab not found"); const exec = await chrome.scripting.executeScript({ target: { tabId: tab.id }, func: async () => { // Content scripts are NOT trusted contexts after the storage // hardening initializer sets TRUSTED_CONTEXTS. This call should // either throw or return an empty object — the key must not // be readable. try { const data = await chrome.storage.session.get("peach_session_dk"); return { accessible: true, hasKey: "peach_session_dk" in data, keys: Object.keys(data), }; } catch (e) { return { accessible: false, error: String(e) }; } }, }); return exec[0]?.result as { accessible: boolean; hasKey?: boolean; keys?: string[]; error?: string; }; }); // The content script must not be able to read the session key. // With TRUSTED_CONTEXTS, chrome.storage.session.get throws. expect(result?.accessible).toBe(false); // Verify the extension page (trusted context) CAN still read it — // proving the key exists but is isolated from content scripts. const extAccess = await popup.evaluate(async () => { try { const data = await chrome.storage.session.get("peach_session_dk"); return { hasKey: "peach_session_dk" in data }; } catch { return { hasKey: false }; } }); expect(extAccess?.hasKey).toBe(true); }); });