From 1ba58d3e00b36ac63c5df637895c7eab77812c4f Mon Sep 17 00:00:00 2001 From: Mhayk Whandson Date: Mon, 20 Jul 2026 12:13:50 +0100 Subject: [PATCH] deps,test: fix undici globals on frozen globalThis When Object.freeze(globalThis) is called before any undici-backed global (fetch, WebSocket, Response, etc.) is accessed, the lazy initialisation in setGlobalDispatcher throws: TypeError: Cannot define property Symbol(undici.globalDispatcher.2), object is not extensible This is a regression introduced when globals were made lazy-loaded (nodejs/node#45659). The Node.js security best practices guide explicitly recommends Object.freeze(globalThis) as a monkey-patching defence (CWE-349), so this failure is particularly unfortunate. Fix: wrap the Object.defineProperty calls in setGlobalDispatcher with a try/catch. When globalThis is not extensible the dispatcher is stored in a module-level fallback variable. getGlobalDispatcher is updated to return globalThis[symbol] ?? fallbackDispatcher so that the normal path (extensible globalThis) is unchanged. The bundled deps/undici/undici.js is updated to match. A test is added in test/parallel/test-undici-frozen-globalthis.js that freezes globalThis before accessing any of the affected globals. Fixes: https://github.com/nodejs/node/issues/46788 --- deps/undici/src/lib/global.js | 41 +++++++++++-------- deps/undici/undici.js | 33 ++++++++------- .../parallel/test-undici-frozen-globalthis.js | 26 ++++++++++++ 3 files changed, 70 insertions(+), 30 deletions(-) create mode 100644 test/parallel/test-undici-frozen-globalthis.js diff --git a/deps/undici/src/lib/global.js b/deps/undici/src/lib/global.js index 81ab7d10195a29..6055478d762320 100644 --- a/deps/undici/src/lib/global.js +++ b/deps/undici/src/lib/global.js @@ -8,6 +8,9 @@ const { InvalidArgumentError } = require('./core/errors') const Agent = require('./dispatcher/agent') const Dispatcher1Wrapper = require('./dispatcher/dispatcher1-wrapper') +// Fallback storage used when globalThis is not extensible (e.g. Object.freeze(globalThis)). +let fallbackDispatcher + if (getGlobalDispatcher() === undefined) { setGlobalDispatcher(new Agent()) } @@ -17,25 +20,31 @@ function setGlobalDispatcher (agent) { throw new InvalidArgumentError('Argument agent must implement Agent') } - Object.defineProperty(globalThis, globalDispatcher, { - value: agent, - writable: true, - enumerable: false, - configurable: false - }) - - const legacyAgent = agent instanceof Dispatcher1Wrapper ? agent : new Dispatcher1Wrapper(agent) - - Object.defineProperty(globalThis, legacyGlobalDispatcher, { - value: legacyAgent, - writable: true, - enumerable: false, - configurable: false - }) + try { + Object.defineProperty(globalThis, globalDispatcher, { + value: agent, + writable: true, + enumerable: false, + configurable: false + }) + + const legacyAgent = agent instanceof Dispatcher1Wrapper ? agent : new Dispatcher1Wrapper(agent) + + Object.defineProperty(globalThis, legacyGlobalDispatcher, { + value: legacyAgent, + writable: true, + enumerable: false, + configurable: false + }) + } catch { + // globalThis is not extensible (e.g. frozen via Object.freeze(globalThis)). + // Fall back to module-level storage so that fetch, WebSocket, etc. still work. + fallbackDispatcher = agent + } } function getGlobalDispatcher () { - return globalThis[globalDispatcher] + return globalThis[globalDispatcher] ?? fallbackDispatcher } // These are the globals that can be installed by undici.install(). diff --git a/deps/undici/undici.js b/deps/undici/undici.js index 02f7f6da92ff22..0d7ca39f81baaa 100644 --- a/deps/undici/undici.js +++ b/deps/undici/undici.js @@ -10470,6 +10470,7 @@ var require_global2 = __commonJS({ var { InvalidArgumentError } = require_errors(); var Agent = require_agent(); var Dispatcher1Wrapper = require_dispatcher1_wrapper(); + var fallbackDispatcher; if (getGlobalDispatcher2() === void 0) { setGlobalDispatcher2(new Agent()); } @@ -10477,23 +10478,27 @@ var require_global2 = __commonJS({ if (!agent || typeof agent.dispatch !== "function") { throw new InvalidArgumentError("Argument agent must implement Agent"); } - Object.defineProperty(globalThis, globalDispatcher, { - value: agent, - writable: true, - enumerable: false, - configurable: false - }); - const legacyAgent = agent instanceof Dispatcher1Wrapper ? agent : new Dispatcher1Wrapper(agent); - Object.defineProperty(globalThis, legacyGlobalDispatcher, { - value: legacyAgent, - writable: true, - enumerable: false, - configurable: false - }); + try { + Object.defineProperty(globalThis, globalDispatcher, { + value: agent, + writable: true, + enumerable: false, + configurable: false + }); + const legacyAgent = agent instanceof Dispatcher1Wrapper ? agent : new Dispatcher1Wrapper(agent); + Object.defineProperty(globalThis, legacyGlobalDispatcher, { + value: legacyAgent, + writable: true, + enumerable: false, + configurable: false + }); + } catch { + fallbackDispatcher = agent; + } } __name(setGlobalDispatcher2, "setGlobalDispatcher"); function getGlobalDispatcher2() { - return globalThis[globalDispatcher]; + return globalThis[globalDispatcher] ?? fallbackDispatcher; } __name(getGlobalDispatcher2, "getGlobalDispatcher"); var installedExports = ( diff --git a/test/parallel/test-undici-frozen-globalthis.js b/test/parallel/test-undici-frozen-globalthis.js new file mode 100644 index 00000000000000..f95bac3f86c1c1 --- /dev/null +++ b/test/parallel/test-undici-frozen-globalthis.js @@ -0,0 +1,26 @@ +'use strict'; + +// Test that globals provided by undici (fetch, WebSocket, etc.) do not throw +// when globalThis has been frozen via Object.freeze(globalThis). +// +// Refs: https://github.com/nodejs/node/issues/46788 + +require('../common'); + +// Freeze globalThis before any undici globals are accessed. +Object.freeze(globalThis); + +const assert = require('node:assert'); + +// Accessing these globals triggers undici's lazy initialisation, which calls +// setGlobalDispatcher internally. With a frozen globalThis, the original code +// threw: "TypeError: Cannot define property Symbol(undici.globalDispatcher.2), +// object is not extensible". The fix uses a module-level fallback instead. +assert.strictEqual(typeof fetch, 'function', 'fetch must be a function'); +assert.strictEqual(typeof WebSocket, 'function', 'WebSocket must be a function'); +assert.strictEqual(typeof Response, 'function', 'Response must be a function'); +assert.strictEqual(typeof Request, 'function', 'Request must be a function'); +assert.strictEqual(typeof Headers, 'function', 'Headers must be a function'); +assert.strictEqual(typeof FormData, 'function', 'FormData must be a function'); + +console.log('All undici globals accessible after Object.freeze(globalThis)');