Refactor plugin client init as a class#65
Conversation
Extracts the closure-based `initialize()` implementation into a `PluginClient` class that implements `PluginInstance`. Internal state (pluginConfig, subscriptions, listeners) becomes private fields and the event helpers (on/off/emit/execPromise) become private methods. The existing `initialize()` function is preserved as a thin wrapper that constructs and returns a new instance, keeping all call sites and tests working without modification. https://claude.ai/code/session_01UWBW4nLCNZEzw48ujVC4oJ
| Object.assign(subscribedWorkbookVars, updatedVariables); | ||
| }, | ||
| ); | ||
| window.addEventListener('message', this.messageListener, false); |
There was a problem hiding this comment.
Semgrep identified an issue in your code:
Missing origin validation on window.addEventListener('message', ...) allows cross-origin attacks to inject malicious messages and manipulate the application state through the event emitter.
More details about this
The messageListener callback receives data from window.addEventListener('message', ...) without validating the origin property of the incoming message. This means any website or malicious script can send a message to this window and trigger arbitrary code execution through the event handler.
Exploit scenario:
- An attacker creates a malicious website and tricks a user into visiting it.
- The attacker's website uses
window.postMessage()to send a message to a window frame running thisPluginClientcode. - Since there's no origin check in
messageListener, the attacker can send a crafted message object withdata.type,data.result, anddata.errorproperties. - The
messageListenercallback executesthis.emit(e.data.type, e.data.result, e.data.error), passing the attacker's controlled data directly into the event system. - If the emitted event (
e.data.type) is handled by code that performs sensitive operations (like config updates or variable changes), the attacker can manipulate the application's behavior or steal data through the subscribed listeners (likesubscribedWorkbookVarsorsubscribedInteractions).
To resolve this comment:
✨ Commit fix suggestion
- Add an allowlist for the expected sender origin before registering the message handler, for example store a value such as
const allowedOrigin = window.location.originor another known trusted origin from configuration. - Update
this.messageListenerto return early unless the incoming message comes from that trusted origin by checkinge.origin.
For example, use a guard likeif (e.origin !== allowedOrigin) return;. - Keep the existing event dispatch only after the origin check, so the handler becomes
this.messageListener = e => { if (e.origin !== allowedOrigin) return; this.emit(e.data.type, e.data.result, e.data.error); };. - Check that the message shape exists before using it so unexpected cross-origin messages do not trigger errors, for example
if (!e.data || !e.data.type) return;. - Alternatively, if this plugin must accept messages from multiple trusted hosts, define a small allowlist such as
const allowedOrigins = ['https://example.com', 'https://app.example.com'];and gate the handler withif (!allowedOrigins.includes(e.origin)) return;.
💬 Ignore this finding
Reply with Semgrep commands to ignore this finding.
/fp <comment>for false positive/ar <comment>for acceptable risk/other <comment>for all other reasons
Alternatively, triage in Semgrep AppSec Platform to ignore the finding created by insufficient-postmessage-origin-validation.
You can view more details about this finding in the Semgrep AppSec Platform.
Replaces `initialize()` call sites in the client test suite with `new PluginClient()` so the tests reflect the class as the primary surface. The `initialize()` function still exists as a thin wrapper, but the tests now exercise the constructor directly. https://claude.ai/code/session_01UWBW4nLCNZEzw48ujVC4oJ
Summary
initialize()implementation inpackages/plugin-sdk/src/client/initialize.tsinto aPluginClient<T>class that implementsPluginInstance<T>.pluginConfig, subscription maps, registered effects, listeners, captured message listener) becomes private class fields; the event helpers (on/off/emit/execPromise) and URL-param parsing become private methods.initialize<T>()is preserved as a thin wrapper that returnsnew PluginClient<T>(), soclient.ts, the React Provider, and the existing test suites inclient/__tests__/initialize.test.tsandreact/__tests__/hooks.test.tsxcontinue to work without modification.Test plan
yarn turbo testinpackages/plugin-sdkpasses (covers the lifecycle, URL-param parsing, init response, config/elements/style APIs, and destroy behavior).yarn turbo typesandyarn lintpass on the package.destroy()(the existing test asserts this).https://claude.ai/code/session_01UWBW4nLCNZEzw48ujVC4oJ
Generated by Claude Code