🐛 frozen property must be returned its actual value rather than bound value (#2285)

This commit is contained in:
Kuitos 2022-09-26 21:59:24 +08:00 committed by GitHub
parent 9a343b4074
commit 73a507f891
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 82 additions and 61 deletions

View File

@ -5,7 +5,7 @@ import {
getDefaultTplWrapper, getDefaultTplWrapper,
getWrapperId, getWrapperId,
getXPathForElement, getXPathForElement,
isPropertyReadonly, isPropertyFrozen,
nextTask, nextTask,
sleep, sleep,
validateExportLifecycle, validateExportLifecycle,
@ -152,13 +152,13 @@ it('should genAppInstanceIdByName work well', () => {
expect(instanceId3).toBe('hello_2'); expect(instanceId3).toBe('hello_2');
}); });
it('should isPropertyReadonly work well', () => { it('should isPropertyFrozen work well', () => {
const a = { const a = {
get name() { get name() {
return 'read only'; return 'read only';
}, },
}; };
expect(isPropertyReadonly(a, 'name')).toBeTruthy(); expect(isPropertyFrozen(a, 'name')).toBeFalsy();
const b = { const b = {
get name() { get name() {
@ -166,19 +166,28 @@ it('should isPropertyReadonly work well', () => {
}, },
set name(_) {}, set name(_) {},
}; };
expect(isPropertyReadonly(b, 'name')).toBeFalsy(); expect(isPropertyFrozen(b, 'name')).toBeFalsy();
const c = {}; const c = {};
Object.defineProperty(c, 'name', { writable: false }); Object.defineProperty(c, 'name', { writable: false });
expect(isPropertyReadonly(c, 'name')).toBeTruthy(); expect(isPropertyFrozen(c, 'name')).toBeTruthy();
const d = {}; const d = {};
Object.defineProperty(d, 'name', { configurable: true }); Object.defineProperty(d, 'name', { configurable: true });
expect(isPropertyReadonly(d, 'name')).toBeTruthy(); expect(isPropertyFrozen(d, 'name')).toBeFalsy();
const e = {}; const e = {};
Object.defineProperty(e, 'name', { writable: true }); Object.defineProperty(e, 'name', { configurable: false });
expect(isPropertyReadonly(e, 'name')).toBeFalsy(); expect(isPropertyFrozen(e, 'name')).toBeTruthy();
expect(isPropertyReadonly(undefined, 'name')).toBeFalsy(); const f = {};
Object.defineProperty(f, 'name', {
get() {
return 'test';
},
configurable: false,
});
expect(isPropertyFrozen(f, 'name')).toBeTruthy();
expect(isPropertyFrozen(undefined, 'name')).toBeFalsy();
}); });

View File

@ -3,7 +3,7 @@
* @since 2020-03-31 * @since 2020-03-31
*/ */
import { isBoundedFunction, isPropertyReadonly } from '../../utils'; import { isBoundedFunction } from '../../utils';
import { getCurrentRunningApp } from '../common'; import { getCurrentRunningApp } from '../common';
import ProxySandbox from '../proxySandbox'; import ProxySandbox from '../proxySandbox';
@ -26,7 +26,7 @@ beforeAll(() => {
}); });
}); });
test('iterator should be worked the same as the raw window', () => { it('iterator should be worked the same as the raw window', () => {
Object.defineProperty(window, 'nonEnumerableValue', { Object.defineProperty(window, 'nonEnumerableValue', {
enumerable: false, enumerable: false,
value: 1, value: 1,
@ -59,7 +59,7 @@ test('iterator should be worked the same as the raw window', () => {
expect(Object.keys(proxy)).toEqual([...Object.keys(window), 'additionalProp']); expect(Object.keys(proxy)).toEqual([...Object.keys(window), 'additionalProp']);
}); });
test('window.self & window.window & window.top & window.parent should equals with sandbox', () => { it('window.self & window.window & window.top & window.parent should equals with sandbox', () => {
const { proxy } = new ProxySandbox('unit-test'); const { proxy } = new ProxySandbox('unit-test');
expect(proxy.self).toBe(proxy); expect(proxy.self).toBe(proxy);
@ -71,12 +71,12 @@ test('window.self & window.window & window.top & window.parent should equals wit
expect(proxy.parent).toBe(proxy); expect(proxy.parent).toBe(proxy);
}); });
test('globalThis should equals with sandbox', () => { it('globalThis should equals with sandbox', () => {
const { proxy } = new ProxySandbox('globalThis'); const { proxy } = new ProxySandbox('globalThis');
expect(proxy.globalThis).toBe(proxy); expect(proxy.globalThis).toBe(proxy);
}); });
test('allow window.top & window.parent to escape sandbox while in iframe', () => { it('allow window.top & window.parent to escape sandbox while in iframe', () => {
// change window.parent to cheat ProxySandbox is in iframe // change window.parent to cheat ProxySandbox is in iframe
Object.defineProperty(window, 'parent', { value: 'parent' }); Object.defineProperty(window, 'parent', { value: 'parent' });
Object.defineProperty(window, 'top', { value: 'top' }); Object.defineProperty(window, 'top', { value: 'top' });
@ -86,7 +86,7 @@ test('allow window.top & window.parent to escape sandbox while in iframe', () =>
expect(proxy.parent).toBe('parent'); expect(proxy.parent).toBe('parent');
}); });
test('eval should never be represented', () => { it('eval should never be represented', () => {
const { proxy } = new ProxySandbox('eval-test'); const { proxy } = new ProxySandbox('eval-test');
// @ts-ignore // @ts-ignore
window.proxy = proxy; window.proxy = proxy;
@ -102,7 +102,7 @@ test('eval should never be represented', () => {
expect(window.testEval).toBeUndefined(); expect(window.testEval).toBeUndefined();
}); });
test('hasOwnProperty should works well', () => { it('hasOwnProperty should works well', () => {
const { proxy } = new ProxySandbox('unit-test'); const { proxy } = new ProxySandbox('unit-test');
proxy.testName = 'kuitos'; proxy.testName = 'kuitos';
@ -120,7 +120,7 @@ test('hasOwnProperty should works well', () => {
}); });
}); });
test('descriptor of non-configurable and non-enumerable property existed in raw window should be the same after modified in sandbox', () => { it('descriptor of non-configurable and non-enumerable property existed in raw window should be the same after modified in sandbox', () => {
Object.defineProperty(window, 'nonConfigurableProp', { configurable: false, writable: true }); Object.defineProperty(window, 'nonConfigurableProp', { configurable: false, writable: true });
// eslint-disable-next-line getter-return // eslint-disable-next-line getter-return
Object.defineProperty(window, 'nonConfigurablePropWithAccessor', { Object.defineProperty(window, 'nonConfigurablePropWithAccessor', {
@ -161,14 +161,14 @@ test('descriptor of non-configurable and non-enumerable property existed in raw
}); });
}); });
test('A property cannot be reported as non-configurable, if it does not exists as an own property of the target object', () => { it('A property cannot be reported as non-configurable, if it does not exists as an own property of the target object', () => {
const { proxy } = new ProxySandbox('non-configurable'); const { proxy } = new ProxySandbox('non-configurable');
Object.defineProperty(window, 'nonConfigurablePropAfterSandboxCreated', { value: 'test', configurable: false }); Object.defineProperty(window, 'nonConfigurablePropAfterSandboxCreated', { value: 'test', configurable: false });
const descriptor = Object.getOwnPropertyDescriptor(proxy, 'nonConfigurablePropAfterSandboxCreated'); const descriptor = Object.getOwnPropertyDescriptor(proxy, 'nonConfigurablePropAfterSandboxCreated');
expect(descriptor?.configurable).toBeTruthy(); expect(descriptor?.configurable).toBeTruthy();
}); });
test('property added by Object.defineProperty should works as expect', () => { it('property added by Object.defineProperty should works as expect', () => {
const { proxy } = new ProxySandbox('object-define-property-test'); const { proxy } = new ProxySandbox('object-define-property-test');
let v: any; let v: any;
@ -197,7 +197,7 @@ test('property added by Object.defineProperty should works as expect', () => {
}); });
}); });
test('defineProperty should added to the target where its descriptor from', () => { it('defineProperty should added to the target where its descriptor from', () => {
Object.defineProperty(window, 'propertyInNativeWindow', { Object.defineProperty(window, 'propertyInNativeWindow', {
get(this: any) { get(this: any) {
// distinguish it from internal target or raw window with property length // distinguish it from internal target or raw window with property length
@ -219,7 +219,7 @@ test('defineProperty should added to the target where its descriptor from', () =
expect(proxy.propertyInNativeWindow).toBe('ifAccessByInternalTargetWillCauseIllegalInvocation'); expect(proxy.propertyInNativeWindow).toBe('ifAccessByInternalTargetWillCauseIllegalInvocation');
}); });
test('hasOwnProperty should always returns same reference', () => { it('hasOwnProperty should always returns same reference', () => {
const proxy = new ProxySandbox('hasOwnProperty-test').proxy as any; const proxy = new ProxySandbox('hasOwnProperty-test').proxy as any;
proxy.testA = {}; proxy.testA = {};
proxy.testB = {}; proxy.testB = {};
@ -227,7 +227,7 @@ test('hasOwnProperty should always returns same reference', () => {
expect(proxy.testA.hasOwnProperty).toBe(proxy.testB.hasOwnProperty); expect(proxy.testA.hasOwnProperty).toBe(proxy.testB.hasOwnProperty);
}); });
test('document and eval accessing should modify the attachDocProxySymbol value every time', () => { it('document and eval accessing should modify the attachDocProxySymbol value every time', () => {
const proxy1 = new ProxySandbox('doc-access-test1').proxy; const proxy1 = new ProxySandbox('doc-access-test1').proxy;
const proxy2 = new ProxySandbox('doc-access-test2').proxy; const proxy2 = new ProxySandbox('doc-access-test2').proxy;
const proxy3 = new ProxySandbox('eval-access-test1').proxy; const proxy3 = new ProxySandbox('eval-access-test1').proxy;
@ -255,7 +255,7 @@ test('document and eval accessing should modify the attachDocProxySymbol value e
expect(eval1).toBe(eval); expect(eval1).toBe(eval);
}); });
test('document attachDocProxySymbol mark should be remove before next task', (done) => { it('document attachDocProxySymbol mark should be remove before next task', (done) => {
const { proxy } = new ProxySandbox('doc-symbol'); const { proxy } = new ProxySandbox('doc-symbol');
// just access // just access
// @ts-ignore // @ts-ignore
@ -270,7 +270,7 @@ test('document attachDocProxySymbol mark should be remove before next task', (do
}); });
}); });
test('document should work well with MutationObserver', (done) => { it('document should work well with MutationObserver', (done) => {
const docProxy = new ProxySandbox('doc').proxy; const docProxy = new ProxySandbox('doc').proxy;
const observer = new MutationObserver((mutations) => { const observer = new MutationObserver((mutations) => {
@ -290,7 +290,7 @@ test('document should work well with MutationObserver', (done) => {
docProxy.document.body.innerHTML = '<div></div>'; docProxy.document.body.innerHTML = '<div></div>';
}); });
test('bounded function should not be rebounded', () => { it('bounded function should not be rebounded', () => {
const proxy = new ProxySandbox('bound-fn-test').proxy as any; const proxy = new ProxySandbox('bound-fn-test').proxy as any;
const fn = () => {}; const fn = () => {};
const boundedFn = fn.bind(null); const boundedFn = fn.bind(null);
@ -302,25 +302,32 @@ test('bounded function should not be rebounded', () => {
expect(isBoundedFunction(proxy.fn1)).toBeTruthy(); expect(isBoundedFunction(proxy.fn1)).toBeTruthy();
}); });
test('readonly property should not be overwrite', () => { it('frozen property should not be overwrite', () => {
const proxy = new ProxySandbox('bound-fn-test').proxy as any; const globalContext = window;
const fn = () => {};
Object.defineProperty(globalContext, 'frozenProp', {
value: fn,
configurable: false,
enumerable: true,
writable: false,
});
const proxy = new ProxySandbox('bound-fn-test', globalContext).proxy as any;
expect(proxy.frozenProp).toBe(fn);
proxy.normalField = 'normalFieldValue'; proxy.normalField = 'normalFieldValue';
Object.defineProperty(proxy, 'frozenPropInProxy', {
Object.defineProperties(proxy, { value: fn,
readOnlyField: { configurable: false,
value: 'readOnlyFieldValue', enumerable: true,
configurable: false, writable: false,
enumerable: true,
writable: false,
},
}); });
expect(isPropertyReadonly(proxy, 'normalField')).toBeFalsy(); expect(proxy.normalField).toBe('normalFieldValue');
expect(isPropertyReadonly(proxy, 'readOnlyField')).toBeTruthy(); expect(proxy.frozenPropInProxy).toBe(fn);
}); });
test('the prototype should be kept while we create a function with prototype on proxy', () => { it('the prototype should be kept while we create a function with prototype on proxy', () => {
const proxy = new ProxySandbox('new-function').proxy as any; const proxy = new ProxySandbox('new-function').proxy as any;
function test() {} function test() {}
@ -330,7 +337,7 @@ test('the prototype should be kept while we create a function with prototype on
expect(proxy.fn.prototype).toBe(test.prototype); expect(proxy.fn.prototype).toBe(test.prototype);
}); });
test('some native window property was defined with getter in safari and firefox, and they will check the caller source', () => { it('some native window property was defined with getter in safari and firefox, and they will check the caller source', () => {
Object.defineProperty(window, 'mockSafariGetterProperty', { Object.defineProperty(window, 'mockSafariGetterProperty', {
get(this: Window) { get(this: Window) {
// distinguish it from internal target or raw window with property length // distinguish it from internal target or raw window with property length
@ -351,7 +358,7 @@ test('some native window property was defined with getter in safari and firefox,
expect(proxy.mockSafariGetterProperty).toBe('getterPropertyInSafariWindow'); expect(proxy.mockSafariGetterProperty).toBe('getterPropertyInSafariWindow');
}); });
test('falsy values should return as expected', () => { it('falsy values should return as expected', () => {
const { proxy } = new ProxySandbox('falsy-value-test'); const { proxy } = new ProxySandbox('falsy-value-test');
proxy.falsevar = false; proxy.falsevar = false;
proxy.nullvar = null; proxy.nullvar = null;

View File

@ -3,7 +3,7 @@
* @since 2020-04-13 * @since 2020-04-13
*/ */
import { isBoundedFunction, isCallable, isConstructable, isPropertyReadonly } from '../utils'; import { isBoundedFunction, isCallable, isConstructable } from '../utils';
type AppInstance = { name: string; window: WindowProxy }; type AppInstance = { name: string; window: WindowProxy };
let currentRunningApp: AppInstance | null = null; let currentRunningApp: AppInstance | null = null;
@ -21,14 +21,15 @@ export function setCurrentRunningApp(appInstance: { name: string; window: Window
} }
const functionBoundedValueMap = new WeakMap<CallableFunction, CallableFunction>(); const functionBoundedValueMap = new WeakMap<CallableFunction, CallableFunction>();
export function getTargetValue(target: any, value: any, p?: any): any {
export function getTargetValue(target: any, value: any): any {
/* /*
isCallable && !isPropertyReadonly && !isBoundedFunction && !isConstructable window.consolewindow.atob Illegal invocation isCallable && !isBoundedFunction && !isConstructable window.consolewindow.atob Illegal invocation
prototype prototype
@warning edge case lodash.isFunction iframe top window @warning edge case lodash.isFunction iframe top window
@warning configurable及writable都为false的readonly属性proxy必须返回原值 @warning configurable及writable都为false的readonly属性proxy必须返回原值
*/ */
if (isCallable(value) && !isPropertyReadonly(target, p) && !isBoundedFunction(value) && !isConstructable(value)) { if (isCallable(value) && !isBoundedFunction(value) && !isConstructable(value)) {
const cachedBoundFunction = functionBoundedValueMap.get(value); const cachedBoundFunction = functionBoundedValueMap.get(value);
if (cachedBoundFunction) { if (cachedBoundFunction) {
return cachedBoundFunction; return cachedBoundFunction;

View File

@ -5,7 +5,7 @@
*/ */
import type { SandBox } from '../interfaces'; import type { SandBox } from '../interfaces';
import { SandBoxType } from '../interfaces'; import { SandBoxType } from '../interfaces';
import { nativeGlobal, nextTask } from '../utils'; import { isPropertyFrozen, nativeGlobal, nextTask } from '../utils';
import { getCurrentRunningApp, getTargetValue, setCurrentRunningApp, unscopedGlobals } from './common'; import { getCurrentRunningApp, getTargetValue, setCurrentRunningApp, unscopedGlobals } from './common';
type SymbolTarget = 'target' | 'globalContext'; type SymbolTarget = 'target' | 'globalContext';
@ -65,7 +65,7 @@ function createFakeWindow(globalContext: Window) {
/* /*
copy the non-configurable property of global to fakeWindow copy the non-configurable property of global to fakeWindow
see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/handler/getOwnPropertyDescriptor 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 exist as an own property of the target object or if it exists as a configurable own property of the target object.
*/ */
Object.getOwnPropertyNames(globalContext) Object.getOwnPropertyNames(globalContext)
.filter((p) => { .filter((p) => {
@ -250,11 +250,14 @@ export default class ProxySandbox implements SandBox {
return eval; return eval;
} }
const value = propertiesWithGetter.has(p) const actualTarget = propertiesWithGetter.has(p) ? globalContext : p in target ? target : globalContext;
? (globalContext as any)[p] const value = actualTarget[p];
: p in target
? (target as any)[p] // frozen value should return directly, see https://github.com/umijs/qiankun/issues/2015
: (globalContext as any)[p]; if (isPropertyFrozen(actualTarget, p)) {
return value;
}
/* Some dom api must be bound to native window, otherwise it would cause exception like 'TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation' /* Some dom api must be bound to native window, otherwise it would cause exception like 'TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation'
See this code: See this code:
const proxy = new Proxy(window, {}); const proxy = new Proxy(window, {});
@ -262,7 +265,7 @@ export default class ProxySandbox implements SandBox {
proxyFetch('https://qiankun.com'); proxyFetch('https://qiankun.com');
*/ */
const boundTarget = useNativeWindowForBindingsProps.get(p) ? nativeGlobal : globalContext; const boundTarget = useNativeWindowForBindingsProps.get(p) ? nativeGlobal : globalContext;
return getTargetValue(boundTarget, value, p); return getTargetValue(boundTarget, value);
}, },
// trap in operator // trap in operator

View File

@ -76,7 +76,7 @@ export function isConstructable(fn: () => any | FunctionConstructor) {
*/ */
const naughtySafari = typeof document.all === 'function' && typeof document.all === 'undefined'; const naughtySafari = typeof document.all === 'function' && typeof document.all === 'undefined';
const callableFnCacheMap = new WeakMap<CallableFunction, boolean>(); const callableFnCacheMap = new WeakMap<CallableFunction, boolean>();
export const isCallable = (fn: any) => { export function isCallable(fn: any) {
if (callableFnCacheMap.has(fn)) { if (callableFnCacheMap.has(fn)) {
return true; return true;
} }
@ -86,7 +86,7 @@ export const isCallable = (fn: any) => {
callableFnCacheMap.set(fn, callable); callableFnCacheMap.set(fn, callable);
} }
return callable; return callable;
}; }
/** /**
* isPropertyReadonly * isPropertyReadonly
@ -94,28 +94,29 @@ export const isCallable = (fn: any) => {
* @param p * @param p
* @returns boolean * @returns boolean
*/ */
const propertyReadonlyCacheMap = new WeakMap<any, Record<PropertyKey, boolean>>(); const frozenPropertyCacheMap = new WeakMap<any, Record<PropertyKey, boolean>>();
export function isPropertyReadonly(target: any, p?: PropertyKey): boolean { export function isPropertyFrozen(target: any, p?: PropertyKey): boolean {
if (!target || !p) { if (!target || !p) {
return false; return false;
} }
const targetPropertiesFromCache = propertyReadonlyCacheMap.get(target) || {}; const targetPropertiesFromCache = frozenPropertyCacheMap.get(target) || {};
if (targetPropertiesFromCache[p]) { if (targetPropertiesFromCache[p]) {
return targetPropertiesFromCache[p]; return targetPropertiesFromCache[p];
} }
const propertyDescriptor = Object.getOwnPropertyDescriptor(target, p); const propertyDescriptor = Object.getOwnPropertyDescriptor(target, p);
const readonly = Boolean( const frozen = Boolean(
propertyDescriptor && propertyDescriptor &&
propertyDescriptor.configurable === false &&
(propertyDescriptor.writable === false || (propertyDescriptor.get && !propertyDescriptor.set)), (propertyDescriptor.writable === false || (propertyDescriptor.get && !propertyDescriptor.set)),
); );
targetPropertiesFromCache[p] = readonly; targetPropertiesFromCache[p] = frozen;
propertyReadonlyCacheMap.set(target, targetPropertiesFromCache); frozenPropertyCacheMap.set(target, targetPropertiesFromCache);
return readonly; return frozen;
} }
const boundedMap = new WeakMap<CallableFunction, boolean>(); const boundedMap = new WeakMap<CallableFunction, boolean>();