From beb1a61a72c6bead6128d6575b8087d87bec7f67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Mon, 6 Jul 2026 06:08:18 -0700 Subject: [PATCH] Harden `window` and `navigator` global property descriptors Summary: The `window` and `navigator` globals were installed with plain property assignments, which produced writable and configurable data properties. This makes the global definitions safer and more spec-compliant: `window` is now non-writable and non-configurable (still enumerable), matching how browsers expose it, and `navigator` is now non-writable (still configurable and enumerable). `self` is unchanged and remains writable, configurable and enumerable. `window` and `self` are now installed on `globalThis`. Also adds Fantom tests that assert the property descriptors of the `window`, `self`, `navigator`, `Infinity`, `NaN` and `undefined` globals so these guarantees don't regress. Changelog: [General][Changed] - Make the `window` global non-writable and non-configurable, and the `navigator` global non-writable Differential Revision: D110759948 --- .../Libraries/Core/setUpGlobals.js | 13 ++-- .../Libraries/Core/setUpNavigator.js | 7 +- ...ultReactNativeEnvironment-Globals-itest.js | 69 +++++++++++++++++++ 3 files changed, 84 insertions(+), 5 deletions(-) diff --git a/packages/react-native/Libraries/Core/setUpGlobals.js b/packages/react-native/Libraries/Core/setUpGlobals.js index 74d8786131dd..0418fc0246ad 100644 --- a/packages/react-native/Libraries/Core/setUpGlobals.js +++ b/packages/react-native/Libraries/Core/setUpGlobals.js @@ -15,14 +15,19 @@ * Sets up global variables for React Native. * You can use this module directly, or just require InitializeCore. */ -if (global.window === undefined) { +if (globalThis.window === undefined) { // $FlowExpectedError[cannot-write] The global isn't writable anywhere but here, where we define it. - global.window = global; + Object.defineProperty(globalThis, 'window', { + value: globalThis, + configurable: false, + enumerable: true, + writable: false, + }); } -if (global.self === undefined) { +if (globalThis.self === undefined) { // $FlowExpectedError[cannot-write] The global isn't writable anywhere but here, where we define it. - global.self = global; + globalThis.self = globalThis; } // Set up process diff --git a/packages/react-native/Libraries/Core/setUpNavigator.js b/packages/react-native/Libraries/Core/setUpNavigator.js index bcf2cbd597b5..1917abd5202c 100644 --- a/packages/react-native/Libraries/Core/setUpNavigator.js +++ b/packages/react-native/Libraries/Core/setUpNavigator.js @@ -15,7 +15,12 @@ const {polyfillObjectProperty} = require('../Utilities/PolyfillFunctions'); const navigator = global.navigator; if (navigator === undefined) { // $FlowExpectedError[cannot-write] The global isn't writable anywhere but here, where we define it. - global.navigator = {product: 'ReactNative'}; + Object.defineProperty(global, 'navigator', { + value: {product: 'ReactNative'}, + configurable: true, + enumerable: true, + writable: false, + }); } else { // see https://github.com/facebook/react-native/issues/10881 polyfillObjectProperty(navigator, 'product', () => 'ReactNative'); diff --git a/packages/react-native/src/private/setup/__tests__/setUpDefaultReactNativeEnvironment-Globals-itest.js b/packages/react-native/src/private/setup/__tests__/setUpDefaultReactNativeEnvironment-Globals-itest.js index c319ddbdc061..2172cb72b6a4 100644 --- a/packages/react-native/src/private/setup/__tests__/setUpDefaultReactNativeEnvironment-Globals-itest.js +++ b/packages/react-native/src/private/setup/__tests__/setUpDefaultReactNativeEnvironment-Globals-itest.js @@ -14,6 +14,25 @@ import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; declare var PerformanceObserverEntryList: unknown; declare var EventCounts: unknown; +function expectGlobalDescriptorToMatch( + name: string, + expected: { + enumerable: boolean, + configurable: boolean, + writable: boolean, + }, +) { + const descriptor = Object.getOwnPropertyDescriptor(globalThis, name); + if (descriptor == null) { + throw new Error(`Expected globalThis.${name} to be defined`); + } + expect({ + enumerable: descriptor.enumerable, + configurable: descriptor.configurable, + writable: descriptor.writable, + }).toEqual(expected); +} + describe('setUpDefaultReactNativeEnvironment (globals)', () => { describe('global object', () => { it('should be exposed as globalThis, global, window and self', () => { @@ -23,6 +42,56 @@ describe('setUpDefaultReactNativeEnvironment (globals)', () => { }); }); + describe('property descriptors', () => { + it('should define window as enumerable but not configurable or writable', () => { + expectGlobalDescriptorToMatch('window', { + enumerable: true, + configurable: false, + writable: false, + }); + }); + + it('should define self as configurable, enumerable and writable', () => { + expectGlobalDescriptorToMatch('self', { + enumerable: true, + configurable: true, + writable: true, + }); + }); + + it('should define navigator as configurable and enumerable but not writable', () => { + expectGlobalDescriptorToMatch('navigator', { + enumerable: true, + configurable: true, + writable: false, + }); + }); + + it('should define Infinity as not configurable, writable or enumerable', () => { + expectGlobalDescriptorToMatch('Infinity', { + enumerable: false, + configurable: false, + writable: false, + }); + }); + + it('should define NaN as not configurable, writable or enumerable', () => { + expectGlobalDescriptorToMatch('NaN', { + enumerable: false, + configurable: false, + writable: false, + }); + }); + + it('should define undefined as not configurable, writable or enumerable', () => { + expectGlobalDescriptorToMatch('undefined', { + enumerable: false, + configurable: false, + writable: false, + }); + }); + }); + describe('environment', () => { it('should provide process.env.NODE_ENV', () => { expect(process.env.NODE_ENV).toBe('development');