From ace113b969daa1c81a2de971dfcb541c583d0202 Mon Sep 17 00:00:00 2001 From: arturovt Date: Thu, 11 Jun 2026 21:45:14 +0300 Subject: [PATCH] fix(zone.js): harden zoneSymbolEventNames and patches against __proto__ key Initialize `zoneSymbolEventNames` and `patches` with `Object.create(null)` instead of `{}`. This is a hardening change rather than a fix for an exploitable vulnerability. Calling `addEventListener('__proto__', fn)` is not directly attacker-controlled; its presence already implies an application bug. However, if such a call does occur, the current implementation can behave unexpectedly depending on the environment. For `zoneSymbolEventNames`, accessing `zoneSymbolEventNames['__proto__']` on a plain object invokes the inherited `__proto__` accessor and returns `Object.prototype`, which is truthy. This causes `prepareEventNames()` to be skipped, leaving `symbolEventName` undefined and eventually leading to a runtime error when `window['undefined'] = []` is executed. In Node.js environments running with `--disable-proto=throw`, the assignment: ```ts id="z8n4qm" zoneSymbolEventNames['__proto__'] = {}; ``` throws immediately because it triggers the disabled `__proto__` setter. The `patches` registry has a similar issue. A `__proto__` key passed to `__load_patch()` bypasses the duplicate-patch check and reaches: ```ts id="f3v7kx" patches['__proto__'] = fn(...); ``` which invokes the `__proto__` setter and changes the prototype of the `patches` object. Using `Object.create(null)` removes the inherited `__proto__` accessor entirely, causing these keys to behave like ordinary properties rather than interacting with JavaScript's prototype machinery. As part of this change, `patches.hasOwnProperty(name)` is also updated to: ```ts id="n2c8wp" Object.prototype.hasOwnProperty.call(patches, name) ``` since null-prototype objects do not inherit `hasOwnProperty`. --- packages/zone.js/lib/common/utils.ts | 3 ++- packages/zone.js/lib/zone-impl.ts | 4 ++-- packages/zone.js/tsconfig.json | 7 ++++--- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/zone.js/lib/common/utils.ts b/packages/zone.js/lib/common/utils.ts index fe30d8528c18..03c9880c453f 100644 --- a/packages/zone.js/lib/common/utils.ts +++ b/packages/zone.js/lib/common/utils.ts @@ -129,7 +129,8 @@ export const isMix: boolean = !isWebWorker && !!(isWindowExists && internalWindow['HTMLElement']); -const zoneSymbolEventNames: {[eventName: string]: string} = {}; +// tslint:disable-next-line:no-toplevel-property-access +const zoneSymbolEventNames: {[eventName: string]: string} = Object.create(null); const enableBeforeunloadSymbol = zoneSymbol('enable_beforeunload'); diff --git a/packages/zone.js/lib/zone-impl.ts b/packages/zone.js/lib/zone-impl.ts index f0e01ee2c0ec..ba2fb0367e3b 100644 --- a/packages/zone.js/lib/zone-impl.ts +++ b/packages/zone.js/lib/zone-impl.ts @@ -814,7 +814,7 @@ export function initZone(): ZoneType { } static __load_patch(name: string, fn: PatchFn, ignoreDuplicate = false): void { - if (patches.hasOwnProperty(name)) { + if (Object.hasOwn(patches, name)) { // `checkDuplicate` option is defined from global variable // so it works for all modules. // `ignoreDuplicate` can work for the specified module @@ -1601,7 +1601,7 @@ export function initZone(): ZoneType { macroTask: 'macroTask' = 'macroTask', eventTask: 'eventTask' = 'eventTask'; - const patches: {[key: string]: any} = {}; + const patches: {[key: string]: any} = Object.create(null); const _api: ZonePrivate = { symbol: __symbol__, currentZoneFrame: () => _currentZoneFrame, diff --git a/packages/zone.js/tsconfig.json b/packages/zone.js/tsconfig.json index e673fa744a3f..c4f34e08f941 100644 --- a/packages/zone.js/tsconfig.json +++ b/packages/zone.js/tsconfig.json @@ -18,7 +18,8 @@ "es2015.iterable", "es2015.promise", "es2015.symbol", - "es2015.symbol.wellknown" - ], - }, + "es2015.symbol.wellknown", + "es2022.object" + ] + } }