Skip to content

test: add unit coverage for PostMessageTransport source validation#536

Open
ochafik wants to merge 3 commits intomainfrom
worktree-agent-af0a8e59
Open

test: add unit coverage for PostMessageTransport source validation#536
ochafik wants to merge 3 commits intomainfrom
worktree-agent-af0a8e59

Conversation

@ochafik
Copy link
Contributor

@ochafik ochafik commented Mar 6, 2026

Summary

src/message-transport.ts implements PostMessageTransport — the iframe↔host communication layer containing the window-identity security boundary (event.source validation). Previously it had zero unit coverage; the E2E security test at tests/e2e/security.spec.ts:335-352 is a no-op because outerIframe.contentDocument is null cross-origin, so the postMessage injection never fires.

This PR adds src/message-transport.test.ts with 18 tests, 30 assertions.

Behaviors covered

Source validation (security boundary at this layer — 4 tests)

  • Messages from the configured eventSource are delivered
  • Messages from a different source are silently dropped — neither onmessage nor onerror fire (no DoS vector for the error handler)
  • Messages with source: null (browser extensions, devtools) are dropped
  • Transport stays alive after rejecting an untrusted message (regression guard)

Message format validation (6 tests)

  • Valid JSON-RPC requests and notifications → onmessage
  • Ambient non-JSON-RPC traffic (react-devtools-hook, strings, null, numbers) → silently ignored, onerror not called
  • Malformed payloads claiming jsonrpc: "2.0" but missing required fields → onerror called, onmessage not called
  • Unset onmessage/onerror handlers don't crash the listener

send() (2 tests)

  • Posts to the configured eventTarget with "*" targetOrigin
  • Multiple sends preserve order

Lifecycle (6 tests)

  • start() registers a message listener on window
  • close() removes the listener — subsequent messages not delivered
  • close() fires onclose if set, doesn't throw if unset
  • Multi-transport isolation: two transports on the same window each accept only messages from their own iframe (realistic host scenario)

Architectural notes

Source-vs-origin validation. PostMessageTransport checks event.source (window identity), not event.origin. This is by design: origin validation lives in the sandbox proxy relay (examples/basic-host/src/sandbox.ts:76,116,127) which sits between the two PostMessageTransport endpoints and knows both sides' origins. At the endpoints:

  • Host side: source === iframe.contentWindow is narrower than an origin check — it picks out one specific iframe, not "anything from port X"
  • App side: can't know its sandbox's expected origin (that's a host deployment detail)

So source is the correct check at this layer. Documented in a comment in the test file.

Three rejection paths. The implementation distinguishes "not JSON-RPC at all" (silent — iframes receive ambient postMessage traffic from devtools/extensions) from "claims jsonrpc: '2.0' but malformed" (surfaced via onerror). Both paths tested explicitly.

Test infrastructure

Uses a minimal in-memory window stub (~20 LOC) rather than adding jsdom/happy-dom — consistent with the existing bun test setup which has no DOM configured. Tests the contract (wrong source → callback not fired) rather than implementation details (log messages, internal state).

Test results

bun test src
 105 pass
 0 fail
 168 expect() calls
Ran 105 tests across 4 files. [307.00ms]

(87 existing + 18 new, 0 regressions)

ochafik added 2 commits March 6, 2026 16:15
PostMessageTransport is the iframe↔host communication layer and contains
the primary security boundary (event.source validation). Previously it
had zero unit coverage — only exercised indirectly via E2E tests, where
the cross-app injection test is a no-op due to cross-origin contentDocument
restrictions.

Covers 18 test cases across:
- Source validation: trusted source delivers, untrusted/null sources
  dropped silently (no onerror DoS vector), transport survives rejection
- Message format: valid requests/notifications delivered; ambient
  non-JSON-RPC traffic (devtools, extensions) silently ignored;
  malformed jsonrpc:'2.0' payloads surface via onerror
- send(): posts to configured target with '*' targetOrigin
- Lifecycle: start() registers listener, close() removes it and fires
  onclose, post-close messages ignored, multi-transport isolation

Uses a minimal in-memory window stub rather than jsdom/happy-dom —
consistent with the existing bun test setup which has no DOM configured.

Note: the transport validates event.source (window identity), NOT
event.origin. send() posts with '*'. Origin is intentionally not part
of this transport's trust model.
- TS2352: cast `{ id: string }` through `unknown` before
  `MessageEventSource` (lines 366, 370) — no structural overlap
- TS80007: drop ineffective `await` on bun's `.resolves` matcher
  (line 355) — it returns void, not Promise<void>
@pkg-pr-new
Copy link

pkg-pr-new bot commented Mar 6, 2026

Open in StackBlitz

@modelcontextprotocol/ext-apps

npm i https://pkg.pr.new/@modelcontextprotocol/ext-apps@536

@modelcontextprotocol/server-basic-preact

npm i https://pkg.pr.new/@modelcontextprotocol/server-basic-preact@536

@modelcontextprotocol/server-basic-react

npm i https://pkg.pr.new/@modelcontextprotocol/server-basic-react@536

@modelcontextprotocol/server-basic-solid

npm i https://pkg.pr.new/@modelcontextprotocol/server-basic-solid@536

@modelcontextprotocol/server-basic-svelte

npm i https://pkg.pr.new/@modelcontextprotocol/server-basic-svelte@536

@modelcontextprotocol/server-basic-vanillajs

npm i https://pkg.pr.new/@modelcontextprotocol/server-basic-vanillajs@536

@modelcontextprotocol/server-basic-vue

npm i https://pkg.pr.new/@modelcontextprotocol/server-basic-vue@536

@modelcontextprotocol/server-budget-allocator

npm i https://pkg.pr.new/@modelcontextprotocol/server-budget-allocator@536

@modelcontextprotocol/server-cohort-heatmap

npm i https://pkg.pr.new/@modelcontextprotocol/server-cohort-heatmap@536

@modelcontextprotocol/server-customer-segmentation

npm i https://pkg.pr.new/@modelcontextprotocol/server-customer-segmentation@536

@modelcontextprotocol/server-debug

npm i https://pkg.pr.new/@modelcontextprotocol/server-debug@536

@modelcontextprotocol/server-map

npm i https://pkg.pr.new/@modelcontextprotocol/server-map@536

@modelcontextprotocol/server-pdf

npm i https://pkg.pr.new/@modelcontextprotocol/server-pdf@536

@modelcontextprotocol/server-scenario-modeler

npm i https://pkg.pr.new/@modelcontextprotocol/server-scenario-modeler@536

@modelcontextprotocol/server-shadertoy

npm i https://pkg.pr.new/@modelcontextprotocol/server-shadertoy@536

@modelcontextprotocol/server-sheet-music

npm i https://pkg.pr.new/@modelcontextprotocol/server-sheet-music@536

@modelcontextprotocol/server-system-monitor

npm i https://pkg.pr.new/@modelcontextprotocol/server-system-monitor@536

@modelcontextprotocol/server-threejs

npm i https://pkg.pr.new/@modelcontextprotocol/server-threejs@536

@modelcontextprotocol/server-transcript

npm i https://pkg.pr.new/@modelcontextprotocol/server-transcript@536

@modelcontextprotocol/server-video-resource

npm i https://pkg.pr.new/@modelcontextprotocol/server-video-resource@536

@modelcontextprotocol/server-wiki-explorer

npm i https://pkg.pr.new/@modelcontextprotocol/server-wiki-explorer@536

commit: 037eb42

@ochafik ochafik changed the title test: add unit coverage for PostMessageTransport origin/source validation test: add unit coverage for PostMessageTransport source validation Mar 6, 2026
@ochafik ochafik marked this pull request as ready for review March 6, 2026 19:43
@ochafik ochafik requested a review from jonathanhefner March 6, 2026 19:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant