Skip to content

Conversation

@ghengeveld
Copy link
Member

@ghengeveld ghengeveld commented Jun 6, 2025

Closes #31650

What I did

Gracefully handle cross-origin errors when accessing a composed Storybook with interactions, by catching the error and disabling debugger controls.

Screenshot 2025-06-06 at 14 25 27

The instrumenter attempts to retain its state when reloading the preview iframe by storing its state on the parent window. This is not allowed on composed Storybooks due to the same-origin policy. In most cases this is not a problem because the iframe will only be reloaded if the story can't be remounted within 3 'ticks' (see StoryRender.teardown()). If this scenario does happen, the instrumenter will lose its state. This is an edge case we can't easily resolve, because the state we're trying to preserve relies on object references and therefore can't be serialized (e.g. for sessionStorage or postMessage/channel).

Because we cannot predict when the iframe needs a full reload, I've decided to assume the worst and disable the debugger controls for composed Storybooks altogether.

Checklist for Contributors

Testing

The changes in this PR are covered in the following automated tests:

  • stories
  • unit tests
  • integration tests
  • end-to-end tests

Manual testing

This section is mandatory for all contributions. If you believe no manual test is necessary, please state so explicitly. Thanks!

Documentation

  • Add or update documentation reflecting your changes
  • If you are deprecating/removing a feature, make sure to update
    MIGRATION.MD

Checklist for Maintainers

  • When this PR is ready for testing, make sure to add ci:normal, ci:merged or ci:daily GH label to it to run a specific set of sandboxes. The particular set of sandboxes can be found in code/lib/cli-storybook/src/sandbox-templates.ts

  • Make sure this PR contains one of the labels below:

    Available labels
    • bug: Internal changes that fixes incorrect behavior.
    • maintenance: User-facing maintenance tasks.
    • dependencies: Upgrading (sometimes downgrading) dependencies.
    • build: Internal-facing build tooling & test updates. Will not show up in release changelog.
    • cleanup: Minor cleanup style change. Will not show up in release changelog.
    • documentation: Documentation only changes. Will not show up in release changelog.
    • feature request: Introducing a new feature.
    • BREAKING CHANGE: Changes that break compatibility in some way with current major version.
    • other: Changes that don't fit in the above categories.

🦋 Canary release

This PR does not have a canary release associated. You can request a canary release of this pull request by mentioning the @storybookjs/core team here.

core team members can create a canary release here or locally with gh workflow run --repo storybookjs/storybook canary-release-pr.yml --field pr=<PR_NUMBER>

Greptile Summary

Fixes cross-origin security issues in Storybook's interactions panel when accessing composed stories by gracefully handling parent window state access restrictions.

  • Added safe window state access methods in code/core/src/instrumenter/instrumenter.ts to handle cross-origin limitations
  • Modified Window interface in code/core/src/instrumenter/typings.d.ts to properly declare global state variables
  • Implements fallback to local-only state when cross-origin access fails, maintaining forward-only debugging capability
  • Gracefully handles cases where iframe reloads occur in composed stories
  • Preserves interaction functionality while limiting some debugging features (like backwards stepping) in cross-origin scenarios

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

2 file(s) reviewed, 1 comment(s)
Edit PR Review Bot Settings | Greptile

@nx-cloud
Copy link

nx-cloud bot commented Jun 6, 2025

View your CI Pipeline Execution ↗ for commit fd2e026.

Command Status Duration Result
nx run-many -t check -c production --parallel=7 ✅ Succeeded 1s View ↗
nx run-many -t build -c production --parallel=3 ✅ Succeeded 2s View ↗

☁️ Nx Cloud last updated this comment at 2025-06-07 07:22:28 UTC

@ghengeveld ghengeveld changed the title Core: Prevent cross-origin error when accessing composed story with interactions Core: Disable interactions debugger on composed stories to avoid cross-origin error Jun 6, 2025
@storybook-app-bot
Copy link

storybook-app-bot bot commented Jun 6, 2025

Package Benchmarks

Commit: fd2e026, ran on 7 June 2025 at 07:28:08 UTC

The following packages have significant changes to their size or dependencies:

storybook

Before After Difference
Dependency count 49 49 0
Self size 31.40 MB 31.38 MB 🎉 -22 KB 🎉
Dependency size 17.41 MB 17.41 MB 0 B
Bundle Size Analyzer Link Link

sb

Before After Difference
Dependency count 50 50 0
Self size 1 KB 1 KB 0 B
Dependency size 48.81 MB 48.79 MB 🎉 -22 KB 🎉
Bundle Size Analyzer Link Link

@storybook/cli

Before After Difference
Dependency count 324 324 0
Self size 244 KB 244 KB 0 B
Dependency size 97.79 MB 97.81 MB 🚨 +18 KB 🚨
Bundle Size Analyzer Link Link

@storybook/codemod

Before After Difference
Dependency count 267 267 0
Self size 31 KB 31 KB 0 B
Dependency size 82.19 MB 82.21 MB 🚨 +22 KB 🚨
Bundle Size Analyzer Link Link

@ghengeveld ghengeveld added the patch:yes Bugfix & documentation PR that need to be picked to main branch label Jun 7, 2025
@shannonmoeller
Copy link

shannonmoeller commented Jun 10, 2025

Why disable it instead of fixing it? The data could be passed from the child to the parent with window.postMessage(message, '*'). Storybook could even be extended to allow configuration of the parent/child origin relationships to make it more secure than just '*'.

https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage


loadParentWindowState = () => {
try {
this.state = global.window?.parent?.__STORYBOOK_ADDON_INTERACTIONS_INSTRUMENTER_STATE__ || {};

Choose a reason for hiding this comment

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

This should be changed to use window.postMessage() to enable safe cross-origin communication.

@shannonmoeller
Copy link

shannonmoeller commented Jun 10, 2025

I have done similar things to fix issues syncing toolbar state across composed storybook instances. I have this in the preview.tsx file of the parent instance of my component library:

// Propagates global updates to composed storybook instances
window.addEventListener('message', (event)  => {
  const message = JSON.parse(event.data?.trim?.() || '{}');

  if (message.key !== 'storybook-channel') return;
  if (message.event?.type !== 'updateGlobals') return;

  window.top?.document.querySelectorAll('iframe').forEach((iframe) => {
    if (iframe.contentWindow === window) return;

    iframe.contentWindow?.postMessage(event.data, '*');
  });
});

@ghengeveld
Copy link
Member Author

ghengeveld commented Jun 11, 2025

@shannonmoeller Unfortunately we can't use postMessage because as far as I know, its serialization method will lose object references. Instrumenter state contains a property callRefsByResult which is a Map<any, CallRef> where the key is the return value of an instrumented function call. This can be anything, including a DOM node reference or other non-serializable data. If this data would get serialized, we'd lose the reference to the original object (e.g. DOM node). callRefsByResult is required in order to track values back to their originating call. We might be able to work around it by rearchitecting how retained state works (i.e. the rows that you see below the current "breakpoint" line when debugging), but that's a much bigger lift.

Comment on lines +737 to +740
get __STORYBOOK_ADDON_INTERACTIONS_INSTRUMENTER_STATE__() {
throw new Error('Blocked');
},
set __STORYBOOK_ADDON_INTERACTIONS_INSTRUMENTER_STATE__(_: unknown) {},
Copy link
Member

Choose a reason for hiding this comment

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

Long term, would be great to stop having to do this somehow.

Accessing global.window.parent should never be done.
Surely this can be done using the channel?

Till then this looks like an improvement to me.

@ghengeveld ghengeveld merged commit 909f244 into next Jun 11, 2025
61 checks passed
@ghengeveld ghengeveld deleted the handle-cross-origin-interations-state branch June 11, 2025 20:24
JReinhold pushed a commit that referenced this pull request Jun 12, 2025
…tions-state

Core: Disable interactions debugger on composed stories to avoid cross-origin error
(cherry picked from commit 909f244)
@github-actions github-actions bot added the patch:done Patch/release PRs already cherry-picked to main/release branch label Jun 12, 2025
@ndelangen ndelangen removed the patch:yes Bugfix & documentation PR that need to be picked to main branch label Oct 28, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug ci:normal patch:done Patch/release PRs already cherry-picked to main/release branch

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Interactions broken in composed storybook instances

4 participants