Skip to content

Refactor plugin client init as a class#65

Draft
Pearce-Ropion wants to merge 2 commits into
mainfrom
claude/exciting-allen-P1QNQ
Draft

Refactor plugin client init as a class#65
Pearce-Ropion wants to merge 2 commits into
mainfrom
claude/exciting-allen-P1QNQ

Conversation

@Pearce-Ropion
Copy link
Copy Markdown
Collaborator

Summary

  • Extracts the closure-based initialize() implementation in packages/plugin-sdk/src/client/initialize.ts into a PluginClient<T> class that implements PluginInstance<T>.
  • Internal state (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 returns new PluginClient<T>(), so client.ts, the React Provider, and the existing test suites in client/__tests__/initialize.test.ts and react/__tests__/hooks.test.tsx continue to work without modification.

Test plan

  • yarn turbo test in packages/plugin-sdk passes (covers the lifecycle, URL-param parsing, init response, config/elements/style APIs, and destroy behavior).
  • yarn turbo types and yarn lint pass on the package.
  • Spot-check: the message listener reference added in the constructor is the same one removed in destroy() (the existing test asserts this).

https://claude.ai/code/session_01UWBW4nLCNZEzw48ujVC4oJ


Generated by Claude Code

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);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. An attacker creates a malicious website and tricks a user into visiting it.
  2. The attacker's website uses window.postMessage() to send a message to a window frame running this PluginClient code.
  3. Since there's no origin check in messageListener, the attacker can send a crafted message object with data.type, data.result, and data.error properties.
  4. The messageListener callback executes this.emit(e.data.type, e.data.result, e.data.error), passing the attacker's controlled data directly into the event system.
  5. 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 (like subscribedWorkbookVars or subscribedInteractions).

To resolve this comment:

✨ Commit fix suggestion
  1. Add an allowlist for the expected sender origin before registering the message handler, for example store a value such as const allowedOrigin = window.location.origin or another known trusted origin from configuration.
  2. Update this.messageListener to return early unless the incoming message comes from that trusted origin by checking e.origin.
    For example, use a guard like if (e.origin !== allowedOrigin) return;.
  3. 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); };.
  4. 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;.
  5. 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 with if (!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
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.

2 participants