From 4f064a4831d39e31516a2637a7bb2cdde517b5ea Mon Sep 17 00:00:00 2001 From: Kuitos Date: Mon, 3 Apr 2023 11:04:51 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20hasOwnProperty=20should=20work?= =?UTF-8?q?=20well=20with=20special=20[[this]]=20binding=20(#2452)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../__tests__/proxySandbox.speedy.test.ts | 7 ++- src/sandbox/__tests__/proxySandbox.test.ts | 3 ++ src/sandbox/proxySandbox.ts | 45 ++++++++++--------- 3 files changed, 32 insertions(+), 23 deletions(-) diff --git a/src/sandbox/__tests__/proxySandbox.speedy.test.ts b/src/sandbox/__tests__/proxySandbox.speedy.test.ts index 68339d4..45e0c80 100644 --- a/src/sandbox/__tests__/proxySandbox.speedy.test.ts +++ b/src/sandbox/__tests__/proxySandbox.speedy.test.ts @@ -49,7 +49,7 @@ it('should throw errors while variable not existed in current global context', ( } }); -it('should never hijack native method of Object.prototype', () => { +it('should never hijack native method of Object.prototype expect hasOwnProperty', () => { const { proxy } = new ProxySandbox('native-object-method'); // @ts-ignore window.proxy = proxy; @@ -60,6 +60,8 @@ it('should never hijack native method of Object.prototype', () => { window.nativeHasOwnCheckResult = hasOwnProperty.call({nativeHas: 123}, 'nativeHas'); window.proxyHasOwnCheck = window.hasOwnProperty.call({nativeHas: '123'}, 'nativeHas'); window.selfCheck = window.hasOwnProperty('nativeHasOwnCheckResult'); + + window.enumerableCheckResult = propertyIsEnumerable.call({nativeHas: 123}, 'nativeHas'); })(mockGlobalThis); } })()`; @@ -67,6 +69,7 @@ it('should never hijack native method of Object.prototype', () => { const geval = eval; geval(code); expect(window.proxy.nativeHasOwnCheckResult).toBeTruthy(); - expect(window.proxy.proxyHasOwnCheck).toBeFalsy(); + expect(window.proxy.proxyHasOwnCheck).toBeTruthy(); expect(window.proxy.selfCheck).toBeTruthy(); + expect(window.proxy.enumerableCheckResult).toBeTruthy(); }); diff --git a/src/sandbox/__tests__/proxySandbox.test.ts b/src/sandbox/__tests__/proxySandbox.test.ts index 387e4be..3cd52f2 100644 --- a/src/sandbox/__tests__/proxySandbox.test.ts +++ b/src/sandbox/__tests__/proxySandbox.test.ts @@ -112,6 +112,9 @@ it('hasOwnProperty should works well', () => { expect(proxy.hasOwnProperty('testName')).toBeTruthy(); expect(window.hasOwnProperty('testName')).toBeFalsy(); + const hasOwnProperty = proxy.hasOwnProperty; + expect(hasOwnProperty.call({ name: 'kuitos' }, 'name')).toBeTruthy(); + expect(Object.getOwnPropertyDescriptor(proxy, 'testName')).toEqual({ value: 'kuitos', configurable: true, diff --git a/src/sandbox/proxySandbox.ts b/src/sandbox/proxySandbox.ts index b7c86d3..5649602 100644 --- a/src/sandbox/proxySandbox.ts +++ b/src/sandbox/proxySandbox.ts @@ -54,9 +54,9 @@ const mockGlobalThis = 'mockGlobalThis'; // these globals should be recorded while accessing every time const accessingSpiedGlobals = ['document', 'top', 'parent', 'eval']; -const overwrittenGlobals = ['window', 'self', 'globalThis'].concat(inTest ? [mockGlobalThis] : []); +const overwrittenGlobals = ['window', 'self', 'globalThis', 'hasOwnProperty'].concat(inTest ? [mockGlobalThis] : []); export const cachedGlobals = Array.from( - new Set(without([...globals, ...overwrittenGlobals, 'requestAnimationFrame'], ...accessingSpiedGlobals)), + new Set(without(globals.concat(overwrittenGlobals).concat('requestAnimationFrame'), ...accessingSpiedGlobals)), ); // transform cachedGlobals to object for faster element check @@ -67,12 +67,13 @@ const cachedGlobalObjects = cachedGlobals.reduce((acc, globalProp) => ({ ...acc, But overwritten globals must not be escaped, otherwise they will be leaked to the global scope. see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/unscopables */ -const unscopables = without(cachedGlobals, ...overwrittenGlobals).reduce( +const unscopables = without(cachedGlobals, ...accessingSpiedGlobals.concat(overwrittenGlobals)).reduce( + (acc, key) => ({ ...acc, [key]: true }), // Notes that babel will transpile spread operator to Object.assign({}, ...args), which will keep the prototype of Object in merged object, // while this result used as Symbol.unscopables, it will make properties in Object.prototype always be escaped from proxy sandbox as unscopables check will look up prototype chain as well, // such as hasOwnProperty, toString, valueOf, etc. - (acc, key) => ({ ...acc, [key]: true }), - {}, + // so we should use Object.create(null) to create a pure object without prototype chain here. + Object.create(null), ); const useNativeWindowForBindingsProps = new Map([ @@ -149,17 +150,11 @@ let activeSandboxCount = 0; export default class ProxySandbox implements SandBox { /** window 值变更记录 */ private updatedValueSet = new Set(); - - name: string; - - type: SandBoxType; - - proxy: WindowProxy; - - sandboxRunning = true; - private document = document; - + name: string; + type: SandBoxType; + proxy: WindowProxy; + sandboxRunning = true; latestSetProp: PropertyKey | null = null; active() { @@ -190,6 +185,10 @@ export default class ProxySandbox implements SandBox { this.sandboxRunning = false; } + public patchDocument(doc: Document) { + this.document = doc; + } + // the descriptor of global variables in whitelist before it been modified globalWhitelistPrevDescriptor: { [p in (typeof globalVariableWhiteList)[number]]: PropertyDescriptor | undefined } = {}; @@ -205,7 +204,6 @@ export default class ProxySandbox implements SandBox { const { fakeWindow, propertiesWithGetter } = createFakeWindow(globalContext, !!speedy); const descriptorTargetMap = new Map(); - const hasOwnProperty = (key: PropertyKey) => fakeWindow.hasOwnProperty(key) || globalContext.hasOwnProperty(key); const proxy = new Proxy(fakeWindow, { set: (target: FakeWindow, p: PropertyKey, value: any): boolean => { @@ -251,7 +249,7 @@ export default class ProxySandbox implements SandBox { this.registerRunningApp(name, proxy); if (p === Symbol.unscopables) return unscopables; - // avoid who using window.window or window.self to escape the sandbox environment to touch the really window + // avoid who using window.window or window.self to escape the sandbox environment to touch the real window // see https://github.com/eligrey/FileSaver.js/blob/master/src/FileSaver.js#L13 if (p === 'window' || p === 'self') { return proxy; @@ -312,7 +310,7 @@ export default class ProxySandbox implements SandBox { /* as the descriptor of top/self/window/mockTop in raw window are configurable but not in proxy target, we need to get it from target to avoid TypeError see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/handler/getOwnPropertyDescriptor - > A property cannot be reported as non-configurable, if it does not existed as an own property of the target object or if it exists as a configurable own property of the target object. + > A property cannot be reported as non-configurable, if it does not exist as an own property of the target object or if it exists as a configurable own property of the target object. */ if (target.hasOwnProperty(p)) { const descriptor = Object.getOwnPropertyDescriptor(target, p); @@ -374,10 +372,15 @@ export default class ProxySandbox implements SandBox { this.proxy = proxy; activeSandboxCount++; - } - public patchDocument(doc: Document) { - this.document = doc; + function hasOwnProperty(this: any, key: PropertyKey): boolean { + // calling from hasOwnProperty.call(obj, key) + if (this !== proxy && this !== null && typeof this === 'object') { + return Object.prototype.hasOwnProperty.call(this, key); + } + + return fakeWindow.hasOwnProperty(key) || globalContext.hasOwnProperty(key); + } } private registerRunningApp(name: string, proxy: Window) {