🐛 hasOwnProperty should work well with special [[this]] binding (#2452)

This commit is contained in:
Kuitos 2023-04-03 11:04:51 +08:00 committed by GitHub
parent 8098d15176
commit 4f064a4831
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 32 additions and 23 deletions

View File

@ -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'); const { proxy } = new ProxySandbox('native-object-method');
// @ts-ignore // @ts-ignore
window.proxy = proxy; window.proxy = proxy;
@ -60,6 +60,8 @@ it('should never hijack native method of Object.prototype', () => {
window.nativeHasOwnCheckResult = hasOwnProperty.call({nativeHas: 123}, 'nativeHas'); window.nativeHasOwnCheckResult = hasOwnProperty.call({nativeHas: 123}, 'nativeHas');
window.proxyHasOwnCheck = window.hasOwnProperty.call({nativeHas: '123'}, 'nativeHas'); window.proxyHasOwnCheck = window.hasOwnProperty.call({nativeHas: '123'}, 'nativeHas');
window.selfCheck = window.hasOwnProperty('nativeHasOwnCheckResult'); window.selfCheck = window.hasOwnProperty('nativeHasOwnCheckResult');
window.enumerableCheckResult = propertyIsEnumerable.call({nativeHas: 123}, 'nativeHas');
})(mockGlobalThis); })(mockGlobalThis);
} }
})()`; })()`;
@ -67,6 +69,7 @@ it('should never hijack native method of Object.prototype', () => {
const geval = eval; const geval = eval;
geval(code); geval(code);
expect(window.proxy.nativeHasOwnCheckResult).toBeTruthy(); expect(window.proxy.nativeHasOwnCheckResult).toBeTruthy();
expect(window.proxy.proxyHasOwnCheck).toBeFalsy(); expect(window.proxy.proxyHasOwnCheck).toBeTruthy();
expect(window.proxy.selfCheck).toBeTruthy(); expect(window.proxy.selfCheck).toBeTruthy();
expect(window.proxy.enumerableCheckResult).toBeTruthy();
}); });

View File

@ -112,6 +112,9 @@ it('hasOwnProperty should works well', () => {
expect(proxy.hasOwnProperty('testName')).toBeTruthy(); expect(proxy.hasOwnProperty('testName')).toBeTruthy();
expect(window.hasOwnProperty('testName')).toBeFalsy(); expect(window.hasOwnProperty('testName')).toBeFalsy();
const hasOwnProperty = proxy.hasOwnProperty;
expect(hasOwnProperty.call({ name: 'kuitos' }, 'name')).toBeTruthy();
expect(Object.getOwnPropertyDescriptor(proxy, 'testName')).toEqual({ expect(Object.getOwnPropertyDescriptor(proxy, 'testName')).toEqual({
value: 'kuitos', value: 'kuitos',
configurable: true, configurable: true,

View File

@ -54,9 +54,9 @@ const mockGlobalThis = 'mockGlobalThis';
// these globals should be recorded while accessing every time // these globals should be recorded while accessing every time
const accessingSpiedGlobals = ['document', 'top', 'parent', 'eval']; 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( 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 // 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. 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 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, // 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, // 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. // 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<PropertyKey, boolean>([ const useNativeWindowForBindingsProps = new Map<PropertyKey, boolean>([
@ -149,17 +150,11 @@ let activeSandboxCount = 0;
export default class ProxySandbox implements SandBox { export default class ProxySandbox implements SandBox {
/** window 值变更记录 */ /** window 值变更记录 */
private updatedValueSet = new Set<PropertyKey>(); private updatedValueSet = new Set<PropertyKey>();
name: string;
type: SandBoxType;
proxy: WindowProxy;
sandboxRunning = true;
private document = document; private document = document;
name: string;
type: SandBoxType;
proxy: WindowProxy;
sandboxRunning = true;
latestSetProp: PropertyKey | null = null; latestSetProp: PropertyKey | null = null;
active() { active() {
@ -190,6 +185,10 @@ export default class ProxySandbox implements SandBox {
this.sandboxRunning = false; this.sandboxRunning = false;
} }
public patchDocument(doc: Document) {
this.document = doc;
}
// the descriptor of global variables in whitelist before it been modified // the descriptor of global variables in whitelist before it been modified
globalWhitelistPrevDescriptor: { [p in (typeof globalVariableWhiteList)[number]]: PropertyDescriptor | undefined } = 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 { fakeWindow, propertiesWithGetter } = createFakeWindow(globalContext, !!speedy);
const descriptorTargetMap = new Map<PropertyKey, SymbolTarget>(); const descriptorTargetMap = new Map<PropertyKey, SymbolTarget>();
const hasOwnProperty = (key: PropertyKey) => fakeWindow.hasOwnProperty(key) || globalContext.hasOwnProperty(key);
const proxy = new Proxy(fakeWindow, { const proxy = new Proxy(fakeWindow, {
set: (target: FakeWindow, p: PropertyKey, value: any): boolean => { set: (target: FakeWindow, p: PropertyKey, value: any): boolean => {
@ -251,7 +249,7 @@ export default class ProxySandbox implements SandBox {
this.registerRunningApp(name, proxy); this.registerRunningApp(name, proxy);
if (p === Symbol.unscopables) return unscopables; 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 // see https://github.com/eligrey/FileSaver.js/blob/master/src/FileSaver.js#L13
if (p === 'window' || p === 'self') { if (p === 'window' || p === 'self') {
return proxy; 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 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 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)) { if (target.hasOwnProperty(p)) {
const descriptor = Object.getOwnPropertyDescriptor(target, p); const descriptor = Object.getOwnPropertyDescriptor(target, p);
@ -374,10 +372,15 @@ export default class ProxySandbox implements SandBox {
this.proxy = proxy; this.proxy = proxy;
activeSandboxCount++; activeSandboxCount++;
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);
} }
public patchDocument(doc: Document) { return fakeWindow.hasOwnProperty(key) || globalContext.hasOwnProperty(key);
this.document = doc; }
} }
private registerRunningApp(name: string, proxy: Window) { private registerRunningApp(name: string, proxy: Window) {