Skip to content

Security hardening: MessageChannel transport for iframes, host-side request guard, documented threat model #3

Description

@kyu-rong

Background

The bridge currently authenticates the counterparty window correctly — IframeHostAdapter checks event.origin and event.source, and the client filters window messages through allowedOrigins. This stops external frames/windows from spoofing messages.

What origin checks cannot address is the same-context listener: any script already running in the parent or child page (third-party tags, ads, a compromised dependency) can register window.addEventListener('message', ...) and observe all bridge traffic, or call the bridge directly. Transport confidentiality against an attacker with script execution is not fully solvable, but the library currently offers neither the standard surface-reduction mechanism nor a last-line-of-defense hook on the host side.

Proposed changes

1. MessageChannel upgrade for the iframe transport

IframeHostAdapter / IframeClientAdapter exchange every message on the shared window listener. Instead:

  • Perform a single window-level handshake that transfers a MessagePort to the child.
  • Route all subsequent traffic through the port.

Ports are held in closures, so a window-level message listener added by any other script sees nothing after the handshake. This is the same model Comlink uses. (Not applicable to the RN transport — there is no MessageChannel across the native boundary.)

Files: packages/core/src/adapters/IframeHostAdapter.ts, packages/core/src/adapters/IframeClientAdapter.ts

2. Host-side request guard

BridgeHost has registerHandler() and observe-only onCall() lifecycle events, but no interception point that can reject a request before it reaches a handler. The client has Axios-style interceptors; the host's handleMessage path has no symmetric hook.

Add a guard (or host request interceptor) that runs before handler dispatch, so apps can enforce policies like:

  • allowlist the WebView's current URL before serving any action
  • restrict sensitive actions to specific screens/origins
  • reject with a typed BridgeError (e.g. UNAUTHORIZED)

The policy content is the app's responsibility; providing the place to put it is the library's.

File: packages/core/src/host/BridgeHost.ts (guard check at the top of handleMessage)

3. SECURITY.md — documented threat model

State explicitly what the library defends against and what it delegates:

  • Defended: cross-window spoofing (origin + source checks), malformed payloads (schema validation at receiving boundaries).
  • Reduced by this issue: same-context eavesdropping on the iframe transport (MessageChannel), unauthorized actions reaching handlers (host guard).
  • Delegated to the app: not sending secrets over the bridge (scoped/one-time tokens instead), third-party script hygiene (CSP, dependency review).

Non-goals

  • Encrypting bridge payloads — an attacker with script execution can call the bridge directly; encryption adds no real boundary.
  • MessageChannel for the React Native transport — not supported across the native boundary; the host guard (release: v0.1.0 (retry) #2) is the mitigation there.

Acceptance criteria

  • Iframe client/host communicate over a MessagePort after handshake; a window-level listener registered post-handshake observes no bridge traffic (integration test).
  • Handshake failure falls back or errors explicitly — no silent hang.
  • BridgeHost guard can reject a request before handler dispatch; rejection reaches the client as a typed BridgeError (unit test).
  • SECURITY.md documents the threat model above.
  • No breaking changes to the public definePlugin / client API.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions