From ab2c29737edf1ee4683e6464027777dd819a2c6a Mon Sep 17 00:00:00 2001 From: Kuitos Date: Thu, 16 Feb 2023 21:30:23 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20compatible=20with=20legacy=20bro?= =?UTF-8?q?wser=20which=20not=20support=20globalThis=20in=20speedy=20mode?= =?UTF-8?q?=20(#2400)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .fatherrc.js | 6 +- src/loader.ts | 4 +- .../__tests__/proxySandbox.speedy.test.ts | 72 +++++++++++++++++++ src/sandbox/common.ts | 4 -- src/sandbox/globals.ts | 2 +- src/sandbox/patchers/dynamicAppend/common.ts | 4 +- src/sandbox/proxySandbox.ts | 48 ++++++++----- 7 files changed, 112 insertions(+), 28 deletions(-) create mode 100644 src/sandbox/__tests__/proxySandbox.speedy.test.ts diff --git a/.fatherrc.js b/.fatherrc.js index 970f613..7f53b17 100644 --- a/.fatherrc.js +++ b/.fatherrc.js @@ -13,7 +13,11 @@ writeFileSync( globalsFilePath, `// generated from https://github.com/sindresorhus/globals/blob/main/globals.json es2015 part // only init its values while Proxy is supported -export const globals = window.Proxy ? ${JSON.stringify(Object.keys(globals.es2015), null, 2)} : [];`, +export const globals = window.Proxy ? ${JSON.stringify( + Object.keys(globals.es2015), + null, + 2, + )}.filter(p => /* just keep the available properties in current window context */ p in window) : [];`, ); export default { diff --git a/src/loader.ts b/src/loader.ts index ca9c9fd..bc83348 100644 --- a/src/loader.ts +++ b/src/loader.ts @@ -18,7 +18,7 @@ import type { ObjectType, } from './interfaces'; import { createSandboxContainer, css } from './sandbox'; -import { scopedGlobals } from './sandbox/common'; +import { cachedGlobals } from './sandbox/proxySandbox'; import { Deferred, genAppInstanceIdByName, @@ -345,7 +345,7 @@ export async function loadApp( // get the lifecycle hooks from module exports const scriptExports: any = await execScripts(global, sandbox && !useLooseSandbox, { - scopedGlobalVariables: speedySandbox ? scopedGlobals : [], + scopedGlobalVariables: speedySandbox ? cachedGlobals : [], }); const { bootstrap, mount, unmount, update } = getLifecyclesFromExports( scriptExports, diff --git a/src/sandbox/__tests__/proxySandbox.speedy.test.ts b/src/sandbox/__tests__/proxySandbox.speedy.test.ts new file mode 100644 index 0000000..68339d4 --- /dev/null +++ b/src/sandbox/__tests__/proxySandbox.speedy.test.ts @@ -0,0 +1,72 @@ +import ProxySandbox from '../proxySandbox'; + +it('should never throw errors although globalThis is unavailable in current global context', () => { + const { proxy } = new ProxySandbox('globalThis-always-available'); + // @ts-ignore + window.proxy = proxy; + + expect('mockGlobalThis' in window).toBe(false); + expect('mockGlobalThis' in proxy).toBe(true); + + const code = `(function() { + with (window.proxy) { + (function(mockGlobalThis){ + mockGlobalThis.testName = 'kuitos'; + })(mockGlobalThis); + } + })()`; + // eslint-disable-next-line no-eval + const geval = eval; + geval(code); + + // @ts-ignore + expect(proxy.testName).toBe('kuitos'); + // @ts-ignore + expect(window.testName).toBeUndefined(); +}); + +it('should throw errors while variable not existed in current global context', () => { + const { proxy } = new ProxySandbox('invalid-throw-error'); + // @ts-ignore + window.proxy = proxy; + + expect('invalidVariable' in window).toBe(false); + expect('invalidVariable' in proxy).toBe(false); + + const code = `(function() { + with (window.proxy) { + (function(mockGlobalThis){ + (0, invalidVariable); + })(mockGlobalThis); + } + })()`; + // eslint-disable-next-line no-eval + const geval = eval; + try { + geval(code); + } catch (e: any) { + expect(e.message).toBe('invalidVariable is not defined'); + } +}); + +it('should never hijack native method of Object.prototype', () => { + const { proxy } = new ProxySandbox('native-object-method'); + // @ts-ignore + window.proxy = proxy; + + const code = `(function() { + with (window.proxy) { + (function(mockGlobalThis){ + window.nativeHasOwnCheckResult = hasOwnProperty.call({nativeHas: 123}, 'nativeHas'); + window.proxyHasOwnCheck = window.hasOwnProperty.call({nativeHas: '123'}, 'nativeHas'); + window.selfCheck = window.hasOwnProperty('nativeHasOwnCheckResult'); + })(mockGlobalThis); + } + })()`; + // eslint-disable-next-line no-eval + const geval = eval; + geval(code); + expect(window.proxy.nativeHasOwnCheckResult).toBeTruthy(); + expect(window.proxy.proxyHasOwnCheck).toBeFalsy(); + expect(window.proxy.selfCheck).toBeTruthy(); +}); diff --git a/src/sandbox/common.ts b/src/sandbox/common.ts index 7063064..6a78a7b 100644 --- a/src/sandbox/common.ts +++ b/src/sandbox/common.ts @@ -4,7 +4,6 @@ */ import { isBoundedFunction, isCallable, isConstructable } from '../utils'; -import { globals } from './globals'; type AppInstance = { name: string; window: WindowProxy }; let currentRunningApp: AppInstance | null = null; @@ -21,9 +20,6 @@ export function setCurrentRunningApp(appInstance: { name: string; window: Window currentRunningApp = appInstance; } -export const overwrittenGlobals = ['window', 'self', 'globalThis']; -export const scopedGlobals = Array.from(new Set([...globals, ...overwrittenGlobals, 'requestAnimationFrame'])); - const functionBoundedValueMap = new WeakMap(); export function getTargetValue(target: any, value: any): any { diff --git a/src/sandbox/globals.ts b/src/sandbox/globals.ts index 57681a2..b0454e7 100644 --- a/src/sandbox/globals.ts +++ b/src/sandbox/globals.ts @@ -59,5 +59,5 @@ export const globals = window.Proxy 'valueOf', 'WeakMap', 'WeakSet', - ] + ].filter((p) => /* just keep the available properties in current window context */ p in window) : []; diff --git a/src/sandbox/patchers/dynamicAppend/common.ts b/src/sandbox/patchers/dynamicAppend/common.ts index 8963a1e..333b920 100644 --- a/src/sandbox/patchers/dynamicAppend/common.ts +++ b/src/sandbox/patchers/dynamicAppend/common.ts @@ -6,7 +6,7 @@ import { execScripts } from 'import-html-entry'; import { isFunction } from 'lodash'; import { frameworkConfiguration } from '../../../apis'; import { qiankunHeadTagName } from '../../../utils'; -import { scopedGlobals } from '../../common'; +import { cachedGlobals } from '../../proxySandbox'; import * as css from '../css'; export const rawHeadAppendChild = HTMLHeadElement.prototype.appendChild; @@ -280,7 +280,7 @@ function getOverwrittenAppendChildOrInsertBefore(opts: { const { fetch } = frameworkConfiguration; const referenceNode = mountDOM.contains(refChild) ? refChild : null; - const scopedGlobalVariables = speedySandbox ? scopedGlobals : []; + const scopedGlobalVariables = speedySandbox ? cachedGlobals : []; if (src) { let isRedfinedCurrentScript = false; diff --git a/src/sandbox/proxySandbox.ts b/src/sandbox/proxySandbox.ts index c769e36..83f4b9b 100644 --- a/src/sandbox/proxySandbox.ts +++ b/src/sandbox/proxySandbox.ts @@ -7,7 +7,7 @@ import { without } from 'lodash'; import type { SandBox } from '../interfaces'; import { SandBoxType } from '../interfaces'; import { isPropertyFrozen, nativeGlobal, nextTask } from '../utils'; -import { overwrittenGlobals, getCurrentRunningApp, getTargetValue, setCurrentRunningApp } from './common'; +import { getCurrentRunningApp, getTargetValue, setCurrentRunningApp } from './common'; import { globals } from './globals'; type SymbolTarget = 'target' | 'globalContext'; @@ -47,17 +47,32 @@ const globalVariableWhiteList: string[] = [ ...variableWhiteListInDev, ]; -// these globals should be recorded in every accessing -const accessingSpiedGlobals = ['document', 'top', 'parent', 'hasOwnProperty', 'eval']; +const inTest = process.env.NODE_ENV === 'test'; +const mockSafariTop = 'mockSafariTop'; +const mockTop = 'mockTop'; +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] : []); +export const cachedGlobals = Array.from( + new Set(without([...globals, ...overwrittenGlobals, 'requestAnimationFrame'], ...accessingSpiedGlobals)), +); + +// transform cachedGlobals to object for faster element check +const cachedGlobalObjects = cachedGlobals.reduce((acc, globalProp) => ({ ...acc, [globalProp]: true }), {}); + /* - variables who are impossible to be overwritten need to be escaped from proxy sandbox for performance reasons. + Variables who are impossible to be overwritten need to be escaped from proxy sandbox for performance reasons. + 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(globals, ...accessingSpiedGlobals, ...overwrittenGlobals).reduce( +const unscopables = without(cachedGlobals, ...overwrittenGlobals).reduce( + // 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 }), - { - __proto__: null, - }, + {}, ); const useNativeWindowForBindingsProps = new Map([ @@ -96,7 +111,7 @@ function createFakeWindow(globalContext: Window) { p === 'parent' || p === 'self' || p === 'window' || - (process.env.NODE_ENV === 'test' && (p === 'mockTop' || p === 'mockSafariTop')) + (inTest && (p === mockTop || p === mockSafariTop)) ) { descriptor.configurable = true; /* @@ -153,7 +168,7 @@ export default class ProxySandbox implements SandBox { ]); } - if (process.env.NODE_ENV === 'test' || --activeSandboxCount === 0) { + if (inTest || --activeSandboxCount === 0) { // reset the global value to the prev value Object.keys(this.globalWhitelistPrevDescriptor).forEach((p) => { const descriptor = this.globalWhitelistPrevDescriptor[p]; @@ -236,15 +251,11 @@ export default class ProxySandbox implements SandBox { } // hijack globalWindow accessing with globalThis keyword - if (p === 'globalThis') { + if (p === 'globalThis' || (inTest && p === mockGlobalThis)) { return proxy; } - if ( - p === 'top' || - p === 'parent' || - (process.env.NODE_ENV === 'test' && (p === 'mockTop' || p === 'mockSafariTop')) - ) { + if (p === 'top' || p === 'parent' || (inTest && (p === mockTop || p === mockSafariTop))) { // if your master app in an iframe context, allow these props escape the sandbox if (globalContext === globalContext.parent) { return proxy; @@ -286,14 +297,15 @@ export default class ProxySandbox implements SandBox { // trap in operator // see https://github.com/styled-components/styled-components/blob/master/packages/styled-components/src/constants.js#L12 has(target: FakeWindow, p: string | number | symbol): boolean { - return p in unscopables || p in target || p in globalContext; + // property in cachedGlobalObjects must return true to avoid escape from get trap + return p in cachedGlobalObjects || p in target || p in globalContext; }, getOwnPropertyDescriptor(target: FakeWindow, p: string | number | symbol): PropertyDescriptor | undefined { /* 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 exists 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 existed 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);