Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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(<App win={crossOriginWin} />);
});

// 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(<App win={crossOriginWin2} />);
});

// 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);
});
});
56 changes: 37 additions & 19 deletions packages/shared/ReactPerformanceTrackProperties.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Loading