diff --git a/packages/react-reconciler/src/__tests__/ReactPerformanceTrack-test.js b/packages/react-reconciler/src/__tests__/ReactPerformanceTrack-test.js
index 5f5defb271cc..0287ef166cb2 100644
--- a/packages/react-reconciler/src/__tests__/ReactPerformanceTrack-test.js
+++ b/packages/react-reconciler/src/__tests__/ReactPerformanceTrack-test.js
@@ -629,4 +629,97 @@ describe('ReactPerformanceTracks', () => {
],
]);
});
+
+ // @gate __DEV__ && enableComponentPerformanceTrack
+ it('does not throw SecurityError when a cross-origin Window is passed as a prop', async () => {
+ // Simulate a cross-origin Window whose property enumeration throws SecurityError,
+ // as happens in browsers when a component receives an iframe.contentWindow
+ // from an iframe with a null/cross origin (e.g. srcdoc="").
+ const createCrossOriginWindow = () => {
+ return new Proxy(
+ {},
+ {
+ ownKeys() {
+ // In browsers, `for...in` on a cross-origin Window throws SecurityError.
+ // We simulate that here so the test can run in JSDOM.
+ throw new Error(
+ "Failed to enumerate the properties of 'Window': " +
+ 'cross-origin access blocked.',
+ );
+ },
+ getOwnPropertyDescriptor(target, prop) {
+ throw new Error(
+ `Failed to read a named property '${String(prop)}' from 'Window': ` +
+ 'cross-origin access blocked.',
+ );
+ },
+ get(target, prop) {
+ if (prop === Symbol.toStringTag) {
+ return 'Window';
+ }
+ throw new Error(
+ `Failed to read a named property '${String(prop)}' from 'Window': ` +
+ 'cross-origin access blocked.',
+ );
+ },
+ },
+ );
+ };
+
+ const crossOriginWin = createCrossOriginWindow();
+
+ const App = function App({win}) {
+ Scheduler.unstable_advanceTime(10);
+ return null;
+ };
+
+ // This should not throw SecurityError and must not corrupt the fiber tree.
+ await act(() => {
+ ReactNoop.render();
+ });
+
+ // First render: mount measure should be recorded.
+ expect(performanceMeasureCalls).toEqual([
+ [
+ 'Mount',
+ {
+ detail: {
+ devtools: {
+ color: 'warning',
+ properties: null,
+ tooltipText: 'Mount',
+ track: 'Components ⚛',
+ },
+ },
+ end: 10,
+ start: 0,
+ },
+ ],
+ ]);
+ performanceMeasureCalls.length = 0;
+
+ // Verify the UI remains responsive by re-rendering with a new cross-origin
+ // Window instance (different reference forces React to diff the win prop).
+ const crossOriginWin2 = createCrossOriginWindow();
+ Scheduler.unstable_advanceTime(10);
+ await act(() => {
+ ReactNoop.render();
+ });
+
+ // Second render: App measure should be recorded. The cross-origin Window
+ // props should appear as '[CrossOriginObject]' placeholders — not throw
+ // SecurityError or corrupt the fiber tree.
+ expect(performanceMeasureCalls).toHaveLength(1);
+ const [measureName, measureOptions] = performanceMeasureCalls[0];
+ // Component-update measures are prefixed with a zero-width space (\u200b).
+ expect(measureName).toBe('\u200bApp');
+ expect(measureOptions.detail.devtools.tooltipText).toBe('App');
+ // The cross-origin Window must degrade to '[CrossOriginObject]', not throw.
+ const properties = measureOptions.detail.devtools.properties;
+ expect(properties).not.toBeNull();
+ const hasCrossOriginPlaceholder = properties.some(([key]) =>
+ key.includes('[CrossOriginObject]'),
+ );
+ expect(hasCrossOriginPlaceholder).toBe(true);
+ });
});
diff --git a/packages/shared/ReactPerformanceTrackProperties.js b/packages/shared/ReactPerformanceTrackProperties.js
index 29aba7282f04..76cb7540bf4d 100644
--- a/packages/shared/ReactPerformanceTrackProperties.js
+++ b/packages/shared/ReactPerformanceTrackProperties.js
@@ -62,31 +62,49 @@ export function addObjectToProperties(
prefix: string,
): void {
let addedProperties = 0;
- for (const key in object) {
- if (hasOwnProperty.call(object, key) && key[0] !== '_') {
- addedProperties++;
- const value = object[key];
- addValueToProperties(key, value, properties, indent, prefix);
- if (addedProperties >= OBJECT_WIDTH_LIMIT) {
- properties.push([
- prefix +
- '\xa0\xa0'.repeat(indent) +
- 'Only ' +
- OBJECT_WIDTH_LIMIT +
- ' properties are shown. React will not log more properties of this object.',
- '',
- ]);
- break;
+ try {
+ for (const key in object) {
+ if (hasOwnProperty.call(object, key) && key[0] !== '_') {
+ addedProperties++;
+ const value = object[key];
+ addValueToProperties(key, value, properties, indent, prefix);
+ if (addedProperties >= OBJECT_WIDTH_LIMIT) {
+ properties.push([
+ prefix +
+ '\xa0\xa0'.repeat(indent) +
+ 'Only ' +
+ OBJECT_WIDTH_LIMIT +
+ ' properties are shown. React will not log more properties of this object.',
+ '',
+ ]);
+ break;
+ }
}
}
+ } catch (e) {
+ // Cross-origin objects (e.g. a Window from a cross-origin iframe) throw
+ // SecurityError when their properties are enumerated. Treat the object as
+ // opaque so the render-logger never corrupts the fiber tree.
+ if (addedProperties === 0) {
+ properties.push([
+ prefix + '\xa0\xa0'.repeat(indent) + '[CrossOriginObject]',
+ '',
+ ]);
+ }
}
}
function readReactElementTypeof(value: Object): mixed {
- // Prevents dotting into $$typeof in opaque origin windows.
- return '$$typeof' in value && hasOwnProperty.call(value, '$$typeof')
- ? value.$$typeof
- : undefined;
+ try {
+ // Prevents dotting into $$typeof in opaque origin windows.
+ return '$$typeof' in value && hasOwnProperty.call(value, '$$typeof')
+ ? value.$$typeof
+ : undefined;
+ } catch (e) {
+ // Cross-origin objects (e.g. a Window from a cross-origin iframe) throw
+ // SecurityError on any property access. Treat as non-React-element.
+ return undefined;
+ }
}
export function addValueToProperties(