From 156070416ae9dc228d6b3a786d14ab658f26d46e Mon Sep 17 00:00:00 2001 From: rwaldron Date: Mon, 16 May 2022 13:25:54 -0400 Subject: [PATCH] fix: update near-membrane-dom to use ShadowRealm, fallback to iframe feat: my attempt fix: ensure that globalObjectShape is used in both paths when provided refactor: split logic for better readability refactor: support linkage of window.__proto__ chain in shadow realm chore: revert debug fix: restore globalShapeObject and globalObjectVirtualizationTarget behavior in createShadowRealmVirtualEnvironment --- .../near-membrane-base/src/environment.ts | 12 +- packages/near-membrane-base/src/intrinsics.ts | 3 + .../near-membrane-dom/src/browser-realm.ts | 115 +++++++++++++++++- .../createvirtualenvironment.spec.js | 8 +- 4 files changed, 128 insertions(+), 10 deletions(-) diff --git a/packages/near-membrane-base/src/environment.ts b/packages/near-membrane-base/src/environment.ts index 80c5f629..dd990cb5 100644 --- a/packages/near-membrane-base/src/environment.ts +++ b/packages/near-membrane-base/src/environment.ts @@ -44,9 +44,9 @@ export class VirtualEnvironment { private readonly blueGetSelectedTarget: GetSelectedTarget; - private readonly blueGetTransferableValue: GetTransferableValue; + private blueGetTransferableValue: GetTransferableValue; - private readonly blueGlobalThisPointer: Pointer; + private blueGlobalThisPointer: Pointer; private readonly redCallableEvaluate: CallableEvaluate; @@ -54,13 +54,13 @@ export class VirtualEnvironment { private readonly redCallableLinkPointers: CallableLinkPointers; - private readonly redCallableSetPrototypeOf: CallableSetPrototypeOf; + private redCallableSetPrototypeOf: CallableSetPrototypeOf; - private readonly redCallableDefineProperties: CallableDefineProperties; + private redCallableDefineProperties: CallableDefineProperties; - private readonly redCallableInstallLazyPropertyDescriptors: CallableInstallLazyPropertyDescriptors; + private redCallableInstallLazyPropertyDescriptors: CallableInstallLazyPropertyDescriptors; - private readonly redGlobalThisPointer: Pointer; + private redGlobalThisPointer: Pointer; constructor(options: VirtualEnvironmentOptions) { if (options === undefined) { diff --git a/packages/near-membrane-base/src/intrinsics.ts b/packages/near-membrane-base/src/intrinsics.ts index aad36013..546cd928 100644 --- a/packages/near-membrane-base/src/intrinsics.ts +++ b/packages/near-membrane-base/src/intrinsics.ts @@ -5,6 +5,9 @@ const { includes: ArrayProtoIncludes } = Array.prototype; const { assign: ObjectAssign } = Object; const { apply: ReflectApply, ownKeys: ReflectOwnKeys } = Reflect; +// @ts-ignore: Prevent cannot find name 'ShadowRealm' error. +export const SUPPORTS_SHADOW_REALM = typeof ShadowRealm === 'function'; + /** * This list must be in sync with ecma-262, anything new added to the global object * should be considered, to decide whether or not they need remapping. The default diff --git a/packages/near-membrane-dom/src/browser-realm.ts b/packages/near-membrane-dom/src/browser-realm.ts index d826d9aa..b05d60c8 100644 --- a/packages/near-membrane-dom/src/browser-realm.ts +++ b/packages/near-membrane-dom/src/browser-realm.ts @@ -1,5 +1,6 @@ import { assignFilteredGlobalDescriptorsFromPropertyDescriptorMap, + CallableEvaluate, createBlueConnector, createRedConnector, getFilteredGlobalOwnKeys, @@ -7,6 +8,7 @@ import { DistortionCallback, Instrumentation, PropertyKeys, + SUPPORTS_SHADOW_REALM, VirtualEnvironment, } from '@locker/near-membrane-base'; @@ -30,10 +32,15 @@ const IFRAME_SANDBOX_ATTRIBUTE_VALUE = 'allow-same-origin allow-scripts'; const ObjectCtor = Object; const TypeErrorCtor = TypeError; const { prototype: DocumentProto } = Document; +const { bind: FunctionProtoBind } = Function.prototype; const { prototype: NodeProto } = Node; const { remove: ElementProtoRemove, setAttribute: ElementProtoSetAttribute } = Element.prototype; const { appendChild: NodeProtoAppendChild } = NodeProto; -const { assign: ObjectAssign } = ObjectCtor; +const { + assign: ObjectAssign, + create: ObjectCreate, + getOwnPropertyDescriptors: ObjectGetOwnPropertyDescriptors, +} = ObjectCtor; // eslint-disable-next-line @typescript-eslint/naming-convention const { __lookupGetter__: ObjectProto__lookupGetter__ } = ObjectCtor.prototype as any; const { apply: ReflectApply } = Reflect; @@ -56,9 +63,14 @@ const HTMLIFrameElementProtoContentWindowGetter = ReflectApply( const NodeProtoLastChildGetter = ReflectApply(ObjectProto__lookupGetter__, NodeProto, [ 'lastChild', ])!; +// @ts-ignore: Prevent cannot find name 'ShadowRealm' error. +const ShadowRealmCtor = SUPPORTS_SHADOW_REALM ? ShadowRealm : undefined; +const ShadowRealmProtoEvaluate: CallableEvaluate | undefined = ShadowRealmCtor?.prototype?.evaluate; +const defaultGlobalOwnKeysRegistry = { __proto__: null }; const docRef = document; let defaultGlobalOwnKeys: PropertyKeys | null = null; +let defaultGlobalPropertyDescriptorMap: PropertyDescriptorMap | null = null; function createDetachableIframe(): HTMLIFrameElement { const iframe = ReflectApply(DocumentProtoCreateElement, docRef, [ @@ -163,4 +175,103 @@ function createIframeVirtualEnvironment( return env; } -export default createIframeVirtualEnvironment; +function createShadowRealmVirtualEnvironment( + globalObject: WindowProxy & typeof globalThis, + globalObjectShape: object | null, + providedOptions?: BrowserEnvironmentOptions +): VirtualEnvironment { + if (typeof globalObject !== 'object' || globalObject === null) { + throw new TypeErrorCtor('Missing global object virtualization target.'); + } + const { + distortionCallback, + endowments, + instrumentation, + // eslint-disable-next-line prefer-object-spread + } = ObjectAssign({ __proto__: null }, providedOptions); + + // If a globalObjectShape has been explicitly specified, reset the + // defaultGlobalPropertyDescriptorMap to null. This will ensure that + // the provided globalObjectShape is used to re-create the cached + // defaultGlobalPropertyDescriptorMap. + if (globalObjectShape !== null) { + defaultGlobalPropertyDescriptorMap = null; + } + if (defaultGlobalPropertyDescriptorMap === null) { + let sourceShapeOrOneTimeWindow = globalObjectShape!; + let sourceIsIframe = false; + if (globalObjectShape === null) { + const oneTimeIframe = createDetachableIframe(); + sourceShapeOrOneTimeWindow = ReflectApply( + HTMLIFrameElementProtoContentWindowGetter, + oneTimeIframe, + [] + )!; + sourceIsIframe = true; + } + defaultGlobalOwnKeys = getFilteredGlobalOwnKeys(sourceShapeOrOneTimeWindow); + if (sourceIsIframe) { + ReflectApply(ElementProtoRemove, sourceShapeOrOneTimeWindow, []); + } + defaultGlobalPropertyDescriptorMap = { + __proto__: null, + } as unknown as PropertyDescriptorMap; + assignFilteredGlobalDescriptorsFromPropertyDescriptorMap( + defaultGlobalPropertyDescriptorMap, + ObjectGetOwnPropertyDescriptors(globalObject) + ); + for (let i = 0, { length } = defaultGlobalOwnKeys; i < length; i += 1) { + defaultGlobalOwnKeysRegistry[defaultGlobalOwnKeys[i]] = true; + } + for (const key in defaultGlobalPropertyDescriptorMap) { + if (!(key in defaultGlobalOwnKeysRegistry)) { + delete defaultGlobalPropertyDescriptorMap[key]; + } + } + } + const blueRefs = getCachedGlobalObjectReferences(globalObject); + // Create a new environment. + const env = new VirtualEnvironment({ + blueConnector: createBlueConnector(globalObject), + distortionCallback, + instrumentation, + redConnector: createRedConnector( + ReflectApply(FunctionProtoBind, ShadowRealmProtoEvaluate, [new ShadowRealmCtor()]) + ), + }); + linkIntrinsics(env, globalObject); + // window + env.link('globalThis'); + // Set globalThis.__proto__ in the sandbox to a proxy of + // globalObject.__proto__ and with this, the entire + // structure around window proto chain should be covered. + env.remapProto(globalObject, blueRefs.WindowProto); + let unsafeBlueDescMap: PropertyDescriptorMap = defaultGlobalPropertyDescriptorMap; + if (globalObject !== window) { + unsafeBlueDescMap = { __proto__: null } as unknown as PropertyDescriptorMap; + assignFilteredGlobalDescriptorsFromPropertyDescriptorMap( + unsafeBlueDescMap, + ObjectGetOwnPropertyDescriptors(globalObject) + ); + for (const key in unsafeBlueDescMap) { + if (!(key in defaultGlobalOwnKeysRegistry)) { + delete unsafeBlueDescMap[key]; + } + } + } + env.remapProperties(blueRefs.window, unsafeBlueDescMap); + if (endowments) { + const filteredEndowments: PropertyDescriptorMap = {}; + assignFilteredGlobalDescriptorsFromPropertyDescriptorMap(filteredEndowments, endowments); + removeWindowDescriptors(filteredEndowments); + env.remapProperties(blueRefs.window, filteredEndowments); + } + // We remap `blueRefs.WindowPropertiesProto` to an empty object because it + // is "magical" in that it provides access to elements by id. + env.remapProto(blueRefs.WindowProto, ObjectCreate(blueRefs.EventTargetProto)); + return env; +} + +export default SUPPORTS_SHADOW_REALM + ? createShadowRealmVirtualEnvironment + : createIframeVirtualEnvironment; diff --git a/test/environment/createvirtualenvironment.spec.js b/test/environment/createvirtualenvironment.spec.js index c5e3e642..68cd240e 100644 --- a/test/environment/createvirtualenvironment.spec.js +++ b/test/environment/createvirtualenvironment.spec.js @@ -1,3 +1,4 @@ +import { SUPPORTS_SHADOW_REALM } from '@locker/near-membrane-base'; import createVirtualEnvironment from '@locker/near-membrane-dom'; describe('createVirtualEnvironment', () => { @@ -29,8 +30,11 @@ describe('createVirtualEnvironment', () => { }); it('options object has keepAlive: true', () => { const count = window.frames.length; - const env = createVirtualEnvironment(window, { keepAlive: true }); - expect(window.frames.length).toBe(count + 1); + const env = createVirtualEnvironment(window, window, { + globalObjectShape: window, + keepAlive: true, + }); + expect(window.frames.length).toBe(SUPPORTS_SHADOW_REALM ? count : count + 1); expect(() => env.evaluate('')).not.toThrow(); }); it('options object has keepAlive: false', () => {