/** * messages.adversarial.test.ts — Layer 3 §3.3 adversarial message handler tests. * * Exercises every payload-bearing handler with null/undefined/empty/nested * payloads, unknown message types, AUTOFILL_QUERY edge cases, and Codex * restore boundary inputs. Parameterized for >=300 tests. * * Where the audit references handlers Peach does not implement * (IMPORT_PEACHSCRIPT / RESTORE_FROM_PEACHSCRIPT), the tests confirm the * default `unknown message type` path responds without crashing — see the * findings doc. */ import { beforeEach, describe, expect, it, vi } from "vitest"; import type { Message, MessageResponse } from "../shared/types"; type StorageShape = Record; type MessageListener = ( msg: unknown, sender: chrome.runtime.MessageSender, sendResponse: (response: MessageResponse) => void ) => boolean; // Payload-bearing handlers we exercise with adversarial payloads. const PAYLOAD_HANDLERS = [ "UNLOCK", "CREATE_VAULT", "ADD_ENTRY", "UPDATE_ENTRY", "DELETE_ENTRY", "BATCH_ADD_ENTRIES", "RESTORE_ENTRIES", "ADD_ADDITIONAL_URL", "RESTORE_ENTRY", "PURGE_ENTRY", "PURGE_EXPIRED_TRASH", "SAVE_NOTE", "DELETE_NOTE", "RECOVER_WITH_WORDS", "VALIDATE_RECOVERY_WORDS", "RESTORE_FROM_CODEX", "AUTOFILL_QUERY", "GET_ENTRY_FOR_FILL", "COPY", "UPDATE_SETTINGS", "APPROVE_PASSKEY", "DENY_PASSKEY", "SET_PASSKEY_PROVIDER_ENABLED", "ACTIVATE_LICENSE", "RESTORE_VAULT_SNAPSHOT", "CHANGE_PASSWORD", "SETUP_DURESS", "VERIFY_MAIN_PASSWORD", "FIRE_CANARY_PING", "ML_PREDICT_BATCH", "SEARCH_VAULT", "GET_PASSKEY_FOR_DOMAIN", ]; describe("message handler adversarial inputs", () => { let storage: StorageShape; let sessionStorage: StorageShape; let listener: MessageListener | undefined; beforeEach(async () => { vi.resetModules(); vi.useRealTimers(); storage = { peach_settings: { breachMonitoring: false } }; sessionStorage = {}; listener = undefined; vi.stubGlobal( "fetch", vi.fn(async () => ({ ok: true, status: 200, text: async () => "" })) ); vi.stubGlobal("__PEACH_E2E_PASSKEYS__", false); const chromeMock = { runtime: { id: "peach-extension", getURL: (p: string) => `chrome-extension://peach-extension/${p}`, onMessage: { addListener: vi.fn((fn: MessageListener) => { listener = fn; }), }, sendMessage: vi.fn(async () => ({ ok: true })), }, tabs: { query: vi.fn((_q: unknown, cb?: (tabs: unknown[]) => void) => { if (typeof cb === "function") { cb([{ id: 1 }]); return; } return Promise.resolve([{ id: 1 }]); }), sendMessage: vi.fn(async () => ({ ok: true })), }, storage: { local: { get: vi.fn(async (key?: string | string[]) => { if (!key) return storage; if (typeof key === "string") return { [key]: storage[key] }; return key.reduce>((acc, k) => { acc[k] = storage[k]; return acc; }, {}); }), set: vi.fn(async (next: Record) => { Object.assign(storage, next); }), remove: vi.fn(async (keys: string | string[]) => { const list = Array.isArray(keys) ? keys : [keys]; for (const key of list) delete storage[key]; }), }, session: { get: vi.fn(async (key?: string | string[]) => { if (!key) return sessionStorage; if (typeof key === "string") return { [key]: sessionStorage[key] }; return key.reduce>((acc, k) => { acc[k] = sessionStorage[k]; return acc; }, {}); }), set: vi.fn(async (next: Record) => { Object.assign(sessionStorage, next); }), remove: vi.fn(async (keys: string | string[]) => { const list = Array.isArray(keys) ? keys : [keys]; for (const key of list) delete sessionStorage[key]; }), clear: vi.fn(async () => { for (const k of Object.keys(sessionStorage)) delete sessionStorage[k]; }), }, }, alarms: { clear: vi.fn(), create: vi.fn(), onAlarm: { addListener: vi.fn() }, }, idle: { setDetectionInterval: vi.fn(), onStateChanged: { addListener: vi.fn() }, }, scripting: { executeScript: vi.fn(async () => []) }, } as unknown as typeof chrome; (globalThis as { chrome?: typeof chrome }).chrome = chromeMock; const messages = await import("./messages"); messages.setupMessageHandler(); const vault = await import("./vault"); await vault.ensureSettingsLoaded(); }); async function dispatch( message: Message, sender: chrome.runtime.MessageSender = { id: "peach-extension", url: "chrome-extension://peach-extension/popup.html", } as chrome.runtime.MessageSender, ): Promise { if (!listener) throw new Error("listener not registered"); return new Promise((resolve) => { const keepOpen = listener!(message, sender, resolve); expect(keepOpen).toBe(true); }); } // PCH-01-018: destructive ops require an extension-page sender; the adversarial // RESTORE_FROM_CODEX payload tests must use this to reach payload validation. const EXTENSION_PAGE_SENDER = { id: "peach-extension", url: "chrome-extension://peach-extension/popup.html", } as chrome.runtime.MessageSender; async function setupUnlockedVault() { const vault = await import("./vault"); await vault.createVault("test-pass-12345"); } // ── Null/undefined/empty/nested payload matrix ────────────────────────── describe("payload no-crash matrix", () => { for (const type of PAYLOAD_HANDLERS) { it(`${type} with null payload does not crash`, async () => { const r = await dispatch({ type, payload: null } as unknown as Message); expect(r).toBeDefined(); }); it(`${type} with undefined payload does not crash`, async () => { const r = await dispatch({ type, payload: undefined, } as unknown as Message); expect(r).toBeDefined(); }); it(`${type} with empty object payload does not crash`, async () => { const r = await dispatch({ type, payload: {} } as unknown as Message); expect(r).toBeDefined(); }); it(`${type} with deeply nested payload does not crash`, async () => { const nested: Record = {}; let cur: Record = nested; for (let i = 0; i < 50; i++) { const child: Record = {}; cur.child = child; cur = child; } const r = await dispatch({ type, payload: nested, } as unknown as Message); expect(r).toBeDefined(); }); it(`${type} with array payload does not crash`, async () => { const r = await dispatch({ type, payload: [1, 2, 3], } as unknown as Message); expect(r).toBeDefined(); }); it(`${type} with string payload does not crash`, async () => { const r = await dispatch({ type, payload: "garbage", } as unknown as Message); expect(r).toBeDefined(); }); it(`${type} with number payload does not crash`, async () => { const r = await dispatch({ type, payload: 42, } as unknown as Message); expect(r).toBeDefined(); }); } }); // ── AUD-007: extended no-crash payload sweep ──────────────────────────── // // Each payload variant runs against every handler in PAYLOAD_HANDLERS and // must produce a structured response — never a thrown exception. This // expands fuzz coverage to >3000 tests without changing handler behavior. describe("AUD-007 extended payload no-crash sweep", () => { const extraPayloads: Array<{ name: string; payload: unknown }> = [ { name: "boolean true", payload: true }, { name: "boolean false", payload: false }, { name: "negative number", payload: -1 }, { name: "NaN", payload: NaN }, { name: "Infinity", payload: Infinity }, { name: "empty string", payload: "" }, { name: "very long string", payload: "A".repeat(10_000) }, { name: "unicode string", payload: "私のパスワード🔑" }, { name: "RTL string", payload: "مفتاح‮" }, { name: "null bytes in string", payload: "a\x00b\x00c" }, { name: "html string", payload: "" }, { name: "sql string", payload: "'; DROP TABLE entries; --" }, { name: "path traversal string", payload: "../../etc/passwd" }, { name: "object with null prototype-ish", payload: { __proto__: null } }, { name: "object with prototype pollution attempt", payload: { __proto__: { polluted: true } } }, { name: "object with constructor pollution", payload: { constructor: { prototype: { polluted: true } } } }, { name: "deeply nested array", payload: [[[[[[[[[[]]]]]]]]]] }, { name: "long array", payload: new Array(1_000).fill(0) }, { name: "mixed array", payload: [null, undefined, 1, "x", true, {}, []] }, { name: "function-like object", payload: { call: () => 1, apply: () => 1 } }, { name: "Date object", payload: new Date(0) }, { name: "BigInt-ish payload", payload: { n: "9007199254740993" } }, { name: "circular-safe placeholder", payload: { self: "[Circular]" } }, ]; for (const { name, payload } of extraPayloads) { for (const type of PAYLOAD_HANDLERS) { it(`${type} with ${name} payload does not crash`, async () => { const r = await dispatch({ type, payload } as unknown as Message); expect(r).toBeDefined(); expect(typeof r.ok).toBe("boolean"); }); } } }); // ── Unknown / malformed message types ─────────────────────────────────── describe("unknown message types", () => { const unknownTypes = [ "TOTALLY_UNKNOWN_TYPE", "", "X", "lowercase_type", "with spaces in type", "with-dashes", "WITH.DOTS", "🌸emoji_type", "Unicode_üñïcödé", "IMPORT_PEACHSCRIPT", "RESTORE_FROM_PEACHSCRIPT", "DESTROY_VAULT_QUIETLY", "ESCALATE_PRIVILEGE", ]; for (const type of unknownTypes) { it(`unknown type "${type}" returns error response`, async () => { const r = await dispatch({ type } as unknown as Message); expect(r).toBeDefined(); expect(r.ok).toBe(false); }); } it("missing type field does not crash", async () => { const r = await dispatch({} as unknown as Message); expect(r).toBeDefined(); }); it("type=null does not crash", async () => { const r = await dispatch({ type: null } as unknown as Message); expect(r).toBeDefined(); }); it("type=number does not crash", async () => { const r = await dispatch({ type: 42 } as unknown as Message); expect(r).toBeDefined(); }); }); // ── Sender validation ──────────────────────────────────────────────────── describe("sender validation", () => { it("foreign extension sender is rejected", async () => { if (!listener) throw new Error("listener not registered"); const r = await new Promise((resolve) => { listener!( { type: "GET_ENTRIES" } as Message, { id: "foreign-extension" } as chrome.runtime.MessageSender, resolve ); }); expect(r.ok).toBe(false); expect(r.error).toBe("invalid_sender"); }); it("sender with no URL is rejected", async () => { if (!listener) throw new Error("listener not registered"); await setupUnlockedVault(); const r = await new Promise((resolve) => { listener!( { type: "GET_ENTRIES" } as Message, {} as chrome.runtime.MessageSender, resolve ); }); expect(r).toMatchObject({ ok: false, error: "invalid_sender" }); }); }); // ── AUTOFILL_QUERY adversarial ────────────────────────────────────────── describe("AUTOFILL_QUERY adversarial", () => { async function withEntries(n: number) { const vault = await import("./vault"); await vault.createVault("test-pass-12345"); for (let i = 0; i < n; i++) { await vault.addEntry({ title: `E${i}`, username: `u${i}`, password: "p", url: `https://example${i}.com`, type: "login", }); } } it("empty domain returns empty results", async () => { await withEntries(3); const r = await dispatch({ type: "AUTOFILL_QUERY", payload: { domain: "" }, }); expect(r.ok).toBe(true); expect((r as { data: unknown[] }).data).toEqual([]); }); const adversarialDomains = [ "*", ".*", "%", "", "' OR '1'='1", "../../../etc/passwd", "\x00github.com", "github.com\x00", "GITHUB.COM", "github.com/path", "https://github.com", "javascript:alert(1)", " ", " ", "\t", "\n", String("a").repeat(5000), ]; for (const domain of adversarialDomains) { it(`adversarial domain ${JSON.stringify(domain.slice(0, 30))} does not crash`, async () => { await withEntries(3); const r = await dispatch({ type: "AUTOFILL_QUERY", payload: { domain }, }); expect(r.ok).toBe(true); }); it(`adversarial domain ${JSON.stringify(domain.slice(0, 30))} does not match all entries`, async () => { await withEntries(5); const r = await dispatch({ type: "AUTOFILL_QUERY", payload: { domain }, }); const data = (r as { data: unknown[] }).data; // No adversarial wildcard should ever match every entry. expect(data.length).toBeLessThan(5); }); } it("locked vault: AUTOFILL_QUERY returns error", async () => { const vault = await import("./vault"); await vault.createVault("test-pass-12345"); vault.lock(); const r = await dispatch({ type: "AUTOFILL_QUERY", payload: { domain: "github.com" }, }); expect(r.ok).toBe(false); }); it("response never leaks privateKeyJwk", async () => { await withEntries(3); const r = await dispatch({ type: "AUTOFILL_QUERY", payload: { domain: "example0.com" }, }); const data = (r as { data: Array> }).data; for (const entry of data) { expect("privateKeyJwk" in entry).toBe(false); expect("passkey" in entry).toBe(false); } }); it("kind=card returns card summaries only (no full PAN)", async () => { const vault = await import("./vault"); await vault.createVault("test-pass-12345"); await vault.addEntry({ type: "card", title: "Visa", cardNumber: "4111111111111111", last4: "1111", cardholderName: "JUNE", expMonth: "12", expYear: "2030", cvc: "123", } as unknown as Parameters[0]); const r = await dispatch({ type: "AUTOFILL_QUERY", payload: { domain: "any.com", kind: "card" }, }); const data = (r as { data: Array> }).data; for (const entry of data) { expect("cardNumber" in entry).toBe(false); expect("cvc" in entry).toBe(false); } }); }); // ── RESTORE_FROM_CODEX adversarial ────────────────────────────────────── describe("RESTORE_FROM_CODEX adversarial", () => { const malformed = [ { name: "empty vaultBlob", overrides: { vaultBlob: "" } }, { name: "garbage vaultBlob", overrides: { vaultBlob: "!!!not-base64!!!" } }, { name: "wrong fingerprint", overrides: { vaultFingerprint: "0".repeat(16) } }, { name: "wrong passphrase", overrides: { masterPassphrase: "definitely wrong" } }, { name: "negative timestamp", overrides: { timestamp: -1 } }, { name: "zero timestamp", overrides: { timestamp: 0 } }, { name: "huge entryCount", overrides: { entryCount: 1_000_000 } }, { name: "negative entryCount", overrides: { entryCount: -5 } }, ]; for (const { name, overrides } of malformed) { it(`malformed payload (${name}) returns ok=false without crash`, async () => { const payload = { vaultBlob: "ZGF0YQ==", timestamp: Date.now(), verifyCode: "PX-TEST-1234", vaultFingerprint: "ABCDEF1234567890", entryCount: 0, masterPassphrase: "secret", ...overrides, }; const r = await dispatch({ type: "RESTORE_FROM_CODEX", payload }, EXTENSION_PAGE_SENDER); expect(r).toBeDefined(); expect(r.ok).toBe(false); }); } it("null payload does not crash", async () => { const r = await dispatch({ type: "RESTORE_FROM_CODEX", payload: null, } as unknown as Message, EXTENSION_PAGE_SENDER); expect(r).toBeDefined(); expect(r.ok).toBe(false); }); }); // ── COPY handler ──────────────────────────────────────────────────────── describe("COPY handler adversarial", () => { for (const text of ["", "x", "a".repeat(10000), "\x00\x00\x00", "🔑"]) { it(`COPY of ${JSON.stringify(text.slice(0, 20))} resolves`, async () => { const r = await dispatch({ type: "COPY", payload: { text, label: "test", alreadyWritten: true }, }); expect(r).toBeDefined(); }); } }); // ── FIRE_CANARY_PING adversarial ──────────────────────────────────────── describe("FIRE_CANARY_PING webhook url validation", () => { const badUrls = [ "javascript:alert(1)", "file:///etc/passwd", "data:text/plain,evil", "", "not a url", "ftp://example.com/hook", ]; for (const url of badUrls) { it(`rejects bad webhook URL ${JSON.stringify(url)}`, async () => { const r = await dispatch({ type: "FIRE_CANARY_PING", payload: { webhookUrl: url, entryTitle: "x", trigger: "test", }, }); expect(r.ok).toBe(false); }); } }); // ── GET_ENTRY_FOR_FILL safety ─────────────────────────────────────────── describe("GET_ENTRY_FOR_FILL safety", () => { it("returns entry without passkey field for login entry", async () => { const vault = await import("./vault"); await vault.createVault("test-pass-12345"); const e = await vault.addEntry({ title: "GitHub", username: "june", password: "x", url: "https://github.com", type: "login", }); const r = await dispatch( { type: "GET_ENTRY_FOR_FILL", payload: { id: e.id } }, { id: "peach-extension", url: "https://github.com/login" }, ); expect(r.ok).toBe(true); const data = (r as { data: Record }).data; expect("passkey" in data).toBe(false); expect("privateKeyJwk" in data).toBe(false); }); it("missing id returns error", async () => { await setupUnlockedVault(); const r = await dispatch({ type: "GET_ENTRY_FOR_FILL", payload: { id: "not-a-real-id" }, }); expect(r.ok).toBe(false); }); it("locked vault returns error", async () => { const vault = await import("./vault"); await vault.createVault("test-pass-12345"); vault.lock(); const r = await dispatch({ type: "GET_ENTRY_FOR_FILL", payload: { id: "x" }, }); expect(r.ok).toBe(false); }); }); // ── GET_ENTRY_FOR_EDIT origin re-validation (PCH-01-006) ──────────────── describe("GET_ENTRY_FOR_EDIT origin re-validation (PCH-01-006)", () => { async function dispatchFrom( message: Message, sender: chrome.runtime.MessageSender, ): Promise { if (!listener) throw new Error("listener not registered"); return new Promise((resolve) => { listener!(message, sender, resolve); }); } async function addGithubEntry() { const vault = await import("./vault"); await vault.createVault("test-pass-12345"); return vault.addEntry({ title: "GitHub", username: "june", password: "x", url: "https://github.com", type: "login", }); } it("tab-context request on the entry's domain returns the full entry", async () => { const e = await addGithubEntry(); const r = await dispatchFrom( { type: "GET_ENTRY_FOR_EDIT", payload: { id: e.id } } as Message, { id: "peach-extension", tab: { url: "https://github.com/login" } } as chrome.runtime.MessageSender, ); expect(r.ok).toBe(true); expect((r as { data: Record }).data.username).toBe("june"); }); it("tab-context request on a mismatched domain returns entry_not_found with no leak", async () => { const e = await addGithubEntry(); const r = await dispatchFrom( { type: "GET_ENTRY_FOR_EDIT", payload: { id: e.id } } as Message, { id: "peach-extension", tab: { url: "https://evil.example/login" } } as chrome.runtime.MessageSender, ); expect(r.ok).toBe(false); expect(r.error).toBe("entry_not_found"); expect((r as { data?: unknown }).data).toBeUndefined(); }); it("explicitSummon allows a cross-domain tab-context request", async () => { const e = await addGithubEntry(); const r = await dispatchFrom( { type: "GET_ENTRY_FOR_EDIT", payload: { id: e.id, explicitSummon: true } } as Message, { id: "peach-extension", tab: { url: "https://evil.example/login" } } as chrome.runtime.MessageSender, ); expect(r.ok).toBe(true); expect((r as { data: Record }).data.username).toBe("june"); }); it("popup context (no tab/url) skips the origin check and returns the entry", async () => { const e = await addGithubEntry(); const r = await dispatchFrom( { type: "GET_ENTRY_FOR_EDIT", payload: { id: e.id } } as Message, { id: "peach-extension" } as chrome.runtime.MessageSender, ); expect(r.ok).toBe(true); expect((r as { data: Record }).data.username).toBe("june"); }); }); // ── Destructive-op sender gate (PCH-01-018) ───────────────────────────── describe("destructive ops gated to extension-page senders (PCH-01-018)", () => { async function dispatchFrom( message: Message, sender: chrome.runtime.MessageSender, ): Promise { if (!listener) throw new Error("listener not registered"); return new Promise((resolve) => { listener!(message, sender, resolve); }); } const DESTRUCTIVE = [ "DESTROY_VAULT", "CLEAR_VAULT_ENTRIES", "RESTORE_VAULT_SNAPSHOT", "RESTORE_FROM_CODEX", ] as const; const SENSITIVE = [ "GET_ENTRIES", "GET_RECOVERY_WORDS", "RESTORE_ENTRIES", "UNPAIR_DEVICE", "CONFIRM_VOUCHED_DEVICE", "ROTATE_SYNC_IDENTITY", ] as const; const contentScript = { id: "peach-extension", url: "https://evil.test/", } as chrome.runtime.MessageSender; const extensionPage = { id: "peach-extension", url: "chrome-extension://peach-extension/popup.html", } as chrome.runtime.MessageSender; for (const type of DESTRUCTIVE) { it(`${type} from a content-script sender is rejected as invalid_sender`, async () => { await setupUnlockedVault(); const r = await dispatchFrom({ type, payload: {} } as Message, contentScript); expect(r).toEqual({ ok: false, error: "invalid_sender" }); }); it(`${type} from an extension-page sender passes the origin gate`, async () => { await setupUnlockedVault(); const r = await dispatchFrom({ type, payload: {} } as Message, extensionPage); // It may still fail downstream validation, but never on the origin gate. expect(r.error).not.toBe("invalid_sender"); }); } for (const type of SENSITIVE) { it(`${type} from a content-script sender is rejected as invalid_sender`, async () => { const r = await dispatchFrom({ type, payload: {} } as Message, contentScript); expect(r).toEqual({ ok: false, error: "invalid_sender" }); }); it(`${type} from an extension-page sender reaches the handler`, async () => { const r = await dispatchFrom({ type, payload: {} } as Message, extensionPage); expect(r.error).not.toBe("invalid_sender"); }); } it("GET_LOCAL_IDENTITY from a content-script sender is rejected (audit 2026-08-20 #4)", async () => { const r = await dispatchFrom({ type: "GET_LOCAL_IDENTITY", payload: {} } as Message, contentScript); expect(r).toEqual({ ok: false, error: "invalid_sender" }); }); }); // ── ADD_ENTRY adversarial when locked ─────────────────────────────────── describe("ADD_ENTRY locked-state safety", () => { it("locked vault: ADD_ENTRY returns vault locked", async () => { const r = await dispatch({ type: "ADD_ENTRY", payload: { title: "x", username: "u", password: "p", url: "https://e.com" }, }); expect(r.ok).toBe(false); }); it("ADD_ENTRY with no password and no passkey succeeds for email-only accounts", async () => { await setupUnlockedVault(); const r = await dispatch({ type: "ADD_ENTRY", payload: { title: "x", username: "u", password: "", url: "https://e.com" }, }); expect(r.ok).toBe(true); }); }); // ── DELETE_ENTRY adversarial ──────────────────────────────────────────── describe("DELETE_ENTRY adversarial", () => { const badIds = [ "", " ", "not-a-uuid", String("a").repeat(10000), "00000000-0000-0000-0000-000000000000", "