Skip to content

Commit

Permalink
🐛not rebind non-native global properties (#2733)
Browse files Browse the repository at this point in the history
  • Loading branch information
kuitos authored Oct 18, 2023
1 parent aa4a75c commit 3524195
Show file tree
Hide file tree
Showing 8 changed files with 825 additions and 66 deletions.
7 changes: 5 additions & 2 deletions .fatherrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,14 @@ 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(
export const globalsInES2015 = window.Proxy ? ${JSON.stringify(
Object.keys(globals.es2015),
null,
2,
)}.filter(p => /* just keep the available properties in current window context */ p in window) : [];`,
)}.filter(p => /* just keep the available properties in current window context */ p in window) : [];
export const globalsInBrowser = ${JSON.stringify(Object.keys(globals.browser), null, 2)};
`,
);

export default {
Expand Down
22 changes: 11 additions & 11 deletions src/sandbox/__tests__/common.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,17 @@
* @since 2021-04-12
*/

import { getTargetValue } from '../common';
import { rebindTarget2Fn } from '../common';

describe('getTargetValue', () => {
it('should work well', () => {
const a1 = getTargetValue(window, undefined);
const a1 = rebindTarget2Fn(window, undefined);
expect(a1).toEqual(undefined);

const a2 = getTargetValue(window, null);
const a2 = rebindTarget2Fn(window, null);
expect(a2).toEqual(null);

const a3 = getTargetValue(window, function bindThis(this: any) {
const a3 = rebindTarget2Fn(window, function bindThis(this: any) {
return this;
});
const a3returns = a3();
Expand All @@ -24,23 +24,23 @@ describe('getTargetValue', () => {
function prototypeAddedAfterFirstInvocation(this: any, field: string) {
this.field = field;
}
const notConstructableFunction = getTargetValue(window, prototypeAddedAfterFirstInvocation);
const notConstructableFunction = rebindTarget2Fn(window, prototypeAddedAfterFirstInvocation);
// `this` of not constructable function will be bound automatically, and it can not be changed by calling with special `this`
const result = {};
notConstructableFunction.call(result, '123');
expect(result).toStrictEqual({});
expect(window.field).toEqual('123');

prototypeAddedAfterFirstInvocation.prototype.addedFn = () => {};
const constructableFunction = getTargetValue(window, prototypeAddedAfterFirstInvocation);
const constructableFunction = rebindTarget2Fn(window, prototypeAddedAfterFirstInvocation);
// `this` coule be available if it be predicated as a constructable function
const result2 = {};
constructableFunction.call(result2, '456');
expect(result2).toStrictEqual({ field: '456' });
// window.field not be affected
expect(window.field).toEqual('123');
// reference should be stable after first running
expect(constructableFunction).toBe(getTargetValue(window, prototypeAddedAfterFirstInvocation));
expect(constructableFunction).toBe(rebindTarget2Fn(window, prototypeAddedAfterFirstInvocation));
});

it('should work well while value have a readonly prototype on its prototype chain', () => {
Expand All @@ -56,7 +56,7 @@ describe('getTargetValue', () => {

Object.setPrototypeOf(callableFunction, functionWithReadonlyPrototype);

const boundFn = getTargetValue(window, callableFunction);
const boundFn = rebindTarget2Fn(window, callableFunction);
expect(boundFn.prototype).toBe(callableFunction.prototype);
});

Expand All @@ -71,9 +71,9 @@ describe('getTargetValue', () => {
},
});

const boundFn1 = getTargetValue(window, callableFunction1);
const boundFn2 = getTargetValue(window, callableFunction2);
const boundFn3 = getTargetValue(window, callableFunction3);
const boundFn1 = rebindTarget2Fn(window, callableFunction1);
const boundFn2 = rebindTarget2Fn(window, callableFunction2);
const boundFn3 = rebindTarget2Fn(window, callableFunction3);

expect(boundFn1.toString()).toBe(callableFunction1.toString());
expect(boundFn2.toString()).toBe(callableFunction2.toString());
Expand Down
58 changes: 30 additions & 28 deletions src/sandbox/__tests__/proxySandbox.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,16 +293,35 @@ it('document should work well with MutationObserver', (done) => {
docProxy.document.body.innerHTML = '<div></div>';
});

it('bounded function should not be rebounded', () => {
const proxy = new ProxySandbox('bound-fn-test').proxy as any;
const fn = () => {};
const boundedFn = fn.bind(null);
proxy.fn1 = fn;
proxy.fn2 = boundedFn;
it('native window function calling should always be bound with window', () => {
window.mockNativeWindowFunction = function mockNativeWindowFunction(this: any) {
if (this !== undefined && this !== window) {
throw new Error('Illegal Invocation!');
}

return 'success';
};

const { proxy } = new ProxySandbox('mustBeBoundWithWindowReference');
expect(proxy.mockNativeWindowFunction()).toBe('success');
});

it('native bounded function should not be rebounded', () => {
const proxy = new ProxySandbox('bound-native-fn-test').proxy as any;
const boundedFn = atob.bind(null);
proxy.atob = boundedFn;

expect(proxy.atob === boundedFn).toBeTruthy();
expect(isBoundedFunction(proxy.atob)).toBeTruthy();
});

it('non-native function should not be rebounded', () => {
const proxy = new ProxySandbox('non-native-fn-bound-test').proxy as any;
function test() {}
proxy.fn = test;

expect(proxy.fn1 === fn).toBeFalsy();
expect(proxy.fn2 === boundedFn).toBeTruthy();
expect(isBoundedFunction(proxy.fn1)).toBeTruthy();
expect(proxy.fn === test).toBeTruthy();
expect(isBoundedFunction(proxy.fn)).toBeFalsy();
});

it('frozen property should not be overwrite', () => {
Expand Down Expand Up @@ -332,12 +351,8 @@ it('frozen property should not be overwrite', () => {

it('the prototype should be kept while we create a function with prototype on proxy', () => {
const proxy = new ProxySandbox('new-function').proxy as any;

function test() {}

proxy.fn = test;
expect(proxy.fn === test).toBeFalsy();
expect(proxy.fn.prototype).toBe(test.prototype);
proxy.CustomEvent = CustomEvent;
expect(proxy.CustomEvent.prototype).toBe(CustomEvent.prototype);
});

it('some native window property was defined with getter in safari and firefox, and they will check the caller source', () => {
Expand Down Expand Up @@ -399,19 +414,6 @@ it('should get current running sandbox proxy correctly', async () => {
});
});

it('native window function calling should always be bound with window', () => {
window.nativeWindowFunction = function nativeWindowFunction(this: any) {
if (this !== undefined && this !== window) {
throw new Error('Illegal Invocation!');
}

return 'success';
};

const { proxy } = new ProxySandbox('mustBeBoundWithWindowReference');
expect(proxy.nativeWindowFunction()).toBe('success');
});

describe('should work well while the property existed in global context before', () => {
it('should not write value while the readonly property existed in global context but not in sandbox', () => {
Object.defineProperty(window, 'readonlyPropertyInGlobalContext', {
Expand Down
30 changes: 15 additions & 15 deletions src/sandbox/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,60 +26,60 @@ export function clearCurrentRunningApp() {

const functionBoundedValueMap = new WeakMap<CallableFunction, CallableFunction>();

export function getTargetValue(target: any, value: any): any {
export function rebindTarget2Fn(target: any, fn: any): any {
/*
仅绑定 isCallable && !isBoundedFunction && !isConstructable 的函数对象,如 window.console、window.atob 这类,不然微应用中调用时会抛出 Illegal invocation 异常
目前没有完美的检测方式,这里通过 prototype 中是否还有可枚举的拓展方法的方式来判断
@warning 这里不要随意替换成别的判断方式,因为可能触发一些 edge case(比如在 lodash.isFunction 在 iframe 上下文中可能由于调用了 top window 对象触发的安全异常)
*/
if (isCallable(value) && !isBoundedFunction(value) && !isConstructable(value)) {
const cachedBoundFunction = functionBoundedValueMap.get(value);
if (isCallable(fn) && !isBoundedFunction(fn) && !isConstructable(fn)) {
const cachedBoundFunction = functionBoundedValueMap.get(fn);
if (cachedBoundFunction) {
return cachedBoundFunction;
}

const boundValue = Function.prototype.bind.call(value, target);
const boundValue = Function.prototype.bind.call(fn, target);

// some callable function has custom fields, we need to copy the own props to boundValue. such as moment function.
Object.getOwnPropertyNames(value).forEach((key) => {
Object.getOwnPropertyNames(fn).forEach((key) => {
// boundValue might be a proxy, we need to check the key whether exist in it
if (!boundValue.hasOwnProperty(key)) {
Object.defineProperty(boundValue, key, Object.getOwnPropertyDescriptor(value, key)!);
Object.defineProperty(boundValue, key, Object.getOwnPropertyDescriptor(fn, key)!);
}
});

// copy prototype if bound function not have but target one have
// as prototype is non-enumerable mostly, we need to copy it from target function manually
if (value.hasOwnProperty('prototype') && !boundValue.hasOwnProperty('prototype')) {
// we should not use assignment operator to set boundValue prototype like `boundValue.prototype = value.prototype`
if (fn.hasOwnProperty('prototype') && !boundValue.hasOwnProperty('prototype')) {
// we should not use assignment operator to set boundValue prototype like `boundValue.prototype = fn.prototype`
// as the assignment will also look up prototype chain while it hasn't own prototype property,
// when the lookup succeed, the assignment will throw an TypeError like `Cannot assign to read only property 'prototype' of function` if its descriptor configured with writable false or just have a getter accessor
// see https://github.com/umijs/qiankun/issues/1121
Object.defineProperty(boundValue, 'prototype', { value: value.prototype, enumerable: false, writable: true });
Object.defineProperty(boundValue, 'prototype', { value: fn.prototype, enumerable: false, writable: true });
}

// Some util, like `function isNative() { return typeof Ctor === 'function' && /native code/.test(Ctor.toString()) }` relies on the original `toString()` result
// but bound functions will always return "function() {[native code]}" for `toString`, which is misleading
if (typeof value.toString === 'function') {
const valueHasInstanceToString = value.hasOwnProperty('toString') && !boundValue.hasOwnProperty('toString');
if (typeof fn.toString === 'function') {
const valueHasInstanceToString = fn.hasOwnProperty('toString') && !boundValue.hasOwnProperty('toString');
const boundValueHasPrototypeToString = boundValue.toString === Function.prototype.toString;

if (valueHasInstanceToString || boundValueHasPrototypeToString) {
const originToStringDescriptor = Object.getOwnPropertyDescriptor(
valueHasInstanceToString ? value : Function.prototype,
valueHasInstanceToString ? fn : Function.prototype,
'toString',
);

Object.defineProperty(boundValue, 'toString', {
...originToStringDescriptor,
...(originToStringDescriptor?.get ? null : { value: () => value.toString() }),
...(originToStringDescriptor?.get ? null : { value: () => fn.toString() }),
});
}
}

functionBoundedValueMap.set(value, boundValue);
functionBoundedValueMap.set(fn, boundValue);
return boundValue;
}

return value;
return fn;
}
Loading

1 comment on commit 3524195

@vercel
Copy link

@vercel vercel bot commented on 3524195 Oct 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.