From 7702e1b0ee83b246a7341f6328ce52dabb9ab86d Mon Sep 17 00:00:00 2001 From: giwaov Date: Mon, 6 Jul 2026 11:48:46 +0100 Subject: [PATCH] Guard PortalTransport message origin --- src/__tests__/transport.test.ts | 73 +++++++++++++++++++++++++++++++++ src/transport.ts | 1 + 2 files changed, 74 insertions(+) create mode 100644 src/__tests__/transport.test.ts diff --git a/src/__tests__/transport.test.ts b/src/__tests__/transport.test.ts new file mode 100644 index 0000000..857943a --- /dev/null +++ b/src/__tests__/transport.test.ts @@ -0,0 +1,73 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { PortalTransport } from "../transport.js"; + +type MessageListener = (event: MessageEvent) => void; + +function installWindow() { + let listener: MessageListener | null = null; + const fakeWindow = { + addEventListener: vi.fn((type: string, cb: MessageListener) => { + if (type === "message") listener = cb; + }), + removeEventListener: vi.fn((type: string, cb: MessageListener) => { + if (type === "message" && listener === cb) listener = null; + }), + parent: { postMessage: vi.fn() }, + location: { origin: "https://plugin.example" }, + }; + vi.stubGlobal("window", fakeWindow); + return { + listener: () => { + if (!listener) throw new Error("message listener was not registered"); + return listener; + }, + fakeWindow, + }; +} + +afterEach(() => { + vi.unstubAllGlobals(); + vi.restoreAllMocks(); +}); + +describe("PortalTransport", () => { + it("ignores messages from unexpected origins", () => { + const { listener } = installWindow(); + const transport = new PortalTransport("https://parent.example"); + const onOpen = vi.fn(); + const onMessage = vi.fn(); + transport.onOpen = onOpen; + transport.onMessage = onMessage; + + transport.connect(); + listener()({ origin: "https://evil.example", data: { type: "ws:open" } } as MessageEvent); + listener()({ + origin: "https://evil.example", + data: { type: "ws:message", payload: { ok: false } }, + } as MessageEvent); + + expect(transport.connected).toBe(false); + expect(onOpen).not.toHaveBeenCalled(); + expect(onMessage).not.toHaveBeenCalled(); + }); + + it("accepts messages from the configured parent origin", () => { + const { listener } = installWindow(); + const transport = new PortalTransport("https://parent.example"); + const onOpen = vi.fn(); + const onMessage = vi.fn(); + transport.onOpen = onOpen; + transport.onMessage = onMessage; + + transport.connect(); + listener()({ origin: "https://parent.example", data: { type: "ws:open" } } as MessageEvent); + listener()({ + origin: "https://parent.example", + data: { type: "ws:message", payload: { ok: true } }, + } as MessageEvent); + + expect(transport.connected).toBe(true); + expect(onOpen).toHaveBeenCalledTimes(1); + expect(onMessage).toHaveBeenCalledWith({ ok: true }); + }); +}); \ No newline at end of file diff --git a/src/transport.ts b/src/transport.ts index 800ec60..8d82ed9 100644 --- a/src/transport.ts +++ b/src/transport.ts @@ -216,6 +216,7 @@ export class PortalTransport implements GameTransport { connect(): void { this.listener = (event: MessageEvent) => { + if (this.parentOrigin !== "*" && event.origin !== this.parentOrigin) return; const data = event.data; if (!data || typeof data.type !== "string") return;