Skip to content

[Perf Tracks] Don't crash on cross-origin Window props#36829

Open
junhyeong9812 wants to merge 1 commit into
react:mainfrom
junhyeong9812:fix/perf-track-cross-origin-window-crash
Open

[Perf Tracks] Don't crash on cross-origin Window props#36829
junhyeong9812 wants to merge 1 commit into
react:mainfrom
junhyeong9812:fix/perf-track-cross-origin-window-crash

Conversation

@junhyeong9812

@junhyeong9812 junhyeong9812 commented Jun 20, 2026

Copy link
Copy Markdown

Note on existing PRs: #36430 already has a PR — #36451 (@sleitor) — which guards the property-enumeration path (addObjectToProperties's for…in) and emits a [CrossOriginObject] placeholder. I'm opening this as an alternative because I think guarding the prop-diff path is the more complete fix: the key in next / key in prev checks in addObjectDiffToProperties trigger [[HasProperty]], which itself throws a SecurityError on a cross-origin Window — a failure mode the enumeration-only guard doesn't cover (and which a get-only test fixture doesn't reproduce). Happy to fold this into #36451 — deferring to the maintainers on which direction to take.


Summary

Fixes #36430.

In DEV builds, the component performance-track logger walks a component's props
on every update (logComponentRenderaddObjectDiffToProperties) and reads
value.$$typeof to detect React elements. When a prop is a cross-origin
Window
— e.g. the contentWindow of an <iframe srcdoc="">, whose origin is
"null"any property access throws a SecurityError. Because the logger runs
inside the commit phase (commitPassiveMountOnFiber), the throw escapes the
commit, corrupts the work-in-progress fiber, and every later render fails with
Should not already be working, freezing the whole UI. Production builds are
unaffected (the logger is DEV-only).

function App() {
  const [win, setWin] = useState(null);
  useEffect(() => setWin(iframeRef.current.contentWindow), []);
  return (
    <>
      <iframe ref={iframeRef} srcDoc="<p>hi</p>" />
      <Child win={win} /> {/* cross-origin Window prop crashes the DEV logger */}
    </>
  );
}

readReactElementTypeof already carried a comment about "opaque origin windows",
but its guard was '$$typeof' in value — and the in operator itself triggers
[[HasProperty]][[GetOwnProperty]], which throws for a cross-origin Window,
so the guard never actually held.

The DEV logger must never throw while iterating props, so this guards the two
property accesses that can reach a cross-origin Window:

  • readReactElementTypeof wraps the $$typeof access (including the in
    check) in try/catch, returning undefined ("not a React element") on throw.
  • safeHas (new) guards the prop diff's key in next / key in prev. A
    cross-origin Window exposes its child frames as enumerable numeric indices,
    so for...in can yield "0", and the in check then throws for an index that
    exists on only one side of the diff. An unreadable key is treated as absent.

Both paths degrade gracefully — an unreadable value is logged as a plain object
or a removed/added key instead of crashing.

How did you test this change?

  • Added two regression tests in ReactPerformanceTrack-test.js that model a
    cross-origin Window faithfully with a Proxy (throws on get / has /
    getOwnPropertyDescriptor for everything except the symbols the engine itself
    probes; null prototype). The existing fixture only trapped get, so
    '$$typeof' in value slipped through and never reproduced the crash.

    • does not crash diffing cross-origin Window props (opaque origin) — a
      childless Window (covers the readReactElementTypeof guard).
    • does not crash diffing cross-origin Windows that contain child frames — a
      Window exposing an enumerable child-frame index (covers the safeHas guard).

    Both tests crash before this change and pass after.

  • yarn test packages/react-reconciler/src/__tests__/ReactPerformanceTrack-test.js
    → 8/8 pass (the 6 existing perf-track tests still pass).

  • yarn test --prod of the same file → 8/8 (the tests are @gated to DEV +
    enableComponentPerformanceTrack, so they are correctly gated out in prod).

  • yarn linc, yarn prettier-check, and yarn flow dom-node pass.

The DEV component performance-track logger reads `$$typeof` and uses `key in
obj` while diffing a component's props. For a cross-origin Window prop these
accesses throw a SecurityError -- the `in` operator triggers [[HasProperty]],
which is blocked for an opaque-origin Window -- and the throw escapes the
commit phase (commitPassiveMountOnFiber) and corrupts the work-in-progress
fiber, freezing the UI with "Should not already be working".

Guard the `$$typeof` read in readReactElementTypeof and the `key in next` /
`key in prev` diff checks through a new safeHas helper, treating an unreadable
value as a non-element and an unreadable key as absent so the DEV-only logger
never throws. A cross-origin Window also exposes child frames as enumerable
numeric indices, so the diff's `in` checks are reached for those too.
@meta-cla meta-cla Bot added the CLA Signed label Jun 20, 2026

@daltino daltino left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This PR ensures React doesn’t break when interacting with cross-origin Window properties by adding proper safeguards to prevent SecurityError exceptions. The updates are well-contained, primarily adding test coverage and refining the logic for accessing such properties in a safe manner. Looks good to merge!

@junhyeong9812

Copy link
Copy Markdown
Author

Thank you so much for taking the time to review this — I really appreciate the kind feedback!

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: [19.2] DEV-build logComponentRender throws SecurityError on cross-origin Window props

2 participants