Skip to content

Commit

Permalink
🐛 hasOwnProperty should work well with special [[this]] binding (#2452)
Browse files Browse the repository at this point in the history
  • Loading branch information
kuitos authored Apr 3, 2023
1 parent 8098d15 commit 4f064a4
Show file tree
Hide file tree
Showing 3 changed files with 29 additions and 20 deletions.
7 changes: 5 additions & 2 deletions src/sandbox/__tests__/proxySandbox.speedy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -60,13 +60,16 @@ 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);
}
})()`;
// eslint-disable-next-line no-eval
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();
});
3 changes: 3 additions & 0 deletions src/sandbox/__tests__/proxySandbox.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
39 changes: 21 additions & 18 deletions src/sandbox/proxySandbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<PropertyKey, boolean>([
Expand Down Expand Up @@ -149,17 +150,11 @@ let activeSandboxCount = 0;
export default class ProxySandbox implements SandBox {
/** window 值变更记录 */
private updatedValueSet = new Set<PropertyKey>();

private document = document;
name: string;

type: SandBoxType;

proxy: WindowProxy;

sandboxRunning = true;

private document = document;

latestSetProp: PropertyKey | null = null;

active() {
Expand Down Expand Up @@ -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 } =
{};
Expand All @@ -205,7 +204,6 @@ export default class ProxySandbox implements SandBox {
const { fakeWindow, propertiesWithGetter } = createFakeWindow(globalContext, !!speedy);

const descriptorTargetMap = new Map<PropertyKey, SymbolTarget>();
const hasOwnProperty = (key: PropertyKey) => fakeWindow.hasOwnProperty(key) || globalContext.hasOwnProperty(key);

const proxy = new Proxy(fakeWindow, {
set: (target: FakeWindow, p: PropertyKey, value: any): boolean => {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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) {
Expand Down

1 comment on commit 4f064a4

@vercel
Copy link

@vercel vercel bot commented on 4f064a4 Apr 3, 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.