From d9a76c60ffc0895f22f4c7a11e2dc59a9285ccf6 Mon Sep 17 00:00:00 2001 From: Rick Waldron Date: Mon, 12 Feb 2024 15:19:26 -0500 Subject: [PATCH] fix: do not remap crypto, Crypto and SubtleCrypto if remapTypedArrays is false (#450) --- packages/near-membrane-dom/src/window.ts | 34 ++- packages/near-membrane-shared/src/Set.ts | 2 +- test/membrane/binary-data.spec.js | 309 ++++++++++++++++++++++- 3 files changed, 319 insertions(+), 26 deletions(-) diff --git a/packages/near-membrane-dom/src/window.ts b/packages/near-membrane-dom/src/window.ts index bdbc220c..9a0d3dc7 100644 --- a/packages/near-membrane-dom/src/window.ts +++ b/packages/near-membrane-dom/src/window.ts @@ -1,9 +1,11 @@ import { + ReflectApply, ReflectDeleteProperty, ReflectGetPrototypeOf, ReflectOwnKeys, - toSafeArray, toSafeWeakMap, + SetCtor, + SetProtoHas, } from '@locker/near-membrane-shared'; import { IS_CHROMIUM_BROWSER, rootWindow } from '@locker/near-membrane-shared-dom'; @@ -64,28 +66,22 @@ export function getCachedGlobalObjectReferences( } export function filterWindowKeys(keys: PropertyKey[], remapTypedArrays: boolean): PropertyKey[] { - const result: PropertyKey[] = toSafeArray([]); + const excludedKeys = new SetCtor(['document', 'location', 'top', 'window']); + // Crypto and typed arrays must be from the same global object + if (remapTypedArrays === false) { + excludedKeys.add('crypto'); + excludedKeys.add('Crypto'); + excludedKeys.add('SubtleCrypto'); + } + const result: PropertyKey[] = []; let resultOffset = 0; for (let i = 0, { length } = keys; i < length; i += 1) { const key = keys[i]; - if ( - // Filter out unforgeable property keys that cannot be installed. - key !== 'document' && - key !== 'location ' && - key !== 'top' && - key !== 'window' && - // Remove other browser specific unforgeables. - key !== 'chrome' - ) { - result[resultOffset++] = key; + if (ReflectApply(SetProtoHas, excludedKeys, [key])) { + continue; } + result[resultOffset++] = key; } - - // Crypto and typed arrays must be from the same global object - if (remapTypedArrays === false) { - result.splice(result.indexOf('Crypto'), 1); - } - return result; } @@ -129,7 +125,9 @@ export function removeWindowDescriptors( ReflectDeleteProperty(unsafeDescs, 'chrome'); // Crypto and typed arrays must be from the same global object if (remapTypedArrays === false) { + ReflectDeleteProperty(unsafeDescs, 'crypto'); ReflectDeleteProperty(unsafeDescs, 'Crypto'); + ReflectDeleteProperty(unsafeDescs, 'SubtleCrypto'); } return unsafeDescs; } diff --git a/packages/near-membrane-shared/src/Set.ts b/packages/near-membrane-shared/src/Set.ts index 29c05fd1..9d5a54c7 100644 --- a/packages/near-membrane-shared/src/Set.ts +++ b/packages/near-membrane-shared/src/Set.ts @@ -4,6 +4,6 @@ export const SetCtor = Set; const { prototype: SetProto } = SetCtor; -export const { add: SetProtoAdd, values: SetProtoValues } = SetProto; +export const { add: SetProtoAdd, has: SetProtoHas, values: SetProtoValues } = SetProto; export const SetProtoSizeGetter = ObjectLookupOwnGetter(SetProto, 'size')!; diff --git a/test/membrane/binary-data.spec.js b/test/membrane/binary-data.spec.js index a8ff4af4..291d4619 100644 --- a/test/membrane/binary-data.spec.js +++ b/test/membrane/binary-data.spec.js @@ -9,7 +9,28 @@ if (typeof Atomics !== 'undefined') { }); env.evaluate(` - const ab = new ArrayBuffer(Int32Array.BYTES_PER_ELEMENT ); + const ab = new ArrayBuffer(Int32Array.BYTES_PER_ELEMENT); + const i32a = new Int32Array(ab); + i32a[0] = 9; + Atomics.add(i32a, 0, 33); // 42 + Atomics.and(i32a, 0, 1); // 0 + Atomics.exchange(i32a, 0, 42); // 42 + Atomics.or(i32a, 0, 1); // 43 + Atomics.store(i32a, 0, 18); // 18 + Atomics.sub(i32a, 0, 10); + Atomics.xor(i32a, 0, 1); + expect(Atomics.load(i32a, 0)).toBe(9); + `); + }); + + it('operates on atomic-friendly typed arrays, when typed arrays are not remapped', () => { + const env = createVirtualEnvironment(window, { + endowments: Object.getOwnPropertyDescriptors({ expect }), + remapTypedArrays: false, + }); + + env.evaluate(` + const ab = new ArrayBuffer(Int32Array.BYTES_PER_ELEMENT); const i32a = new Int32Array(ab); i32a[0] = 9; Atomics.add(i32a, 0, 33); // 42 @@ -31,6 +52,21 @@ describe('Blob', () => { endowments: Object.getOwnPropertyDescriptors({ done, expect }), }); + env.evaluate(` + const a = new Uint8Array([97, 98, 99]); + const b = new Blob([a], { type: 'application/octet-stream' }); + b.text().then((output) => { + expect(output).toBe('abc'); + done(); + }); + `); + }); + it('encodes blobs from typed arrays, when typed arrays are not remapped', (done) => { + const env = createVirtualEnvironment(window, { + endowments: Object.getOwnPropertyDescriptors({ done, expect }), + remapTypedArrays: false, + }); + env.evaluate(` const a = new Uint8Array([97, 98, 99]); const b = new Blob([a], { type: 'application/octet-stream' }); @@ -43,20 +79,48 @@ describe('Blob', () => { }); describe('Crypto', () => { - it('creates random values from typed arrays', () => { + it('creates random values from typed arrays', (done) => { const env = createVirtualEnvironment(window, { - endowments: Object.getOwnPropertyDescriptors({ expect }), + endowments: Object.getOwnPropertyDescriptors({ done, expect }), }); env.evaluate(` expect(() => { crypto.getRandomValues(new Uint8Array(1)); }).not.toThrow(); + async function f() { + const algorithm = { name: "RSA-OAEP" }; + const data = new Uint8Array([255]); + const keyPair = await window.crypto.subtle.generateKey( + { + name: "RSA-OAEP", + modulusLength: 4096, + publicExponent: new Uint8Array([1, 0, 1]), + hash: "SHA-256", + }, + true, + ["encrypt", "decrypt"], + ); + + const encrypted = await crypto.subtle.encrypt( + algorithm, + keyPair.publicKey, + data + ); + + const decrypted = await crypto.subtle.decrypt( + algorithm, + keyPair.privateKey, + encrypted + ); + } + + f().then(done); `); }); - it('works when typed arrays are not remapped', () => { + it('creates random values from typed arrays, when typed arrays are not remapped', (done) => { const env = createVirtualEnvironment(window, { - endowments: Object.getOwnPropertyDescriptors({ expect }), + endowments: Object.getOwnPropertyDescriptors({ done, expect }), remapTypedArrays: false, }); @@ -64,11 +128,39 @@ describe('Crypto', () => { expect(() => { crypto.getRandomValues(new Uint8Array(1)); }).not.toThrow(); + async function f() { + const algorithm = { name: "RSA-OAEP" }; + const data = new Uint8Array([255]); + const keyPair = await window.crypto.subtle.generateKey( + { + name: "RSA-OAEP", + modulusLength: 4096, + publicExponent: new Uint8Array([1, 0, 1]), + hash: "SHA-256", + }, + true, + ["encrypt", "decrypt"], + ); + + const encrypted = await crypto.subtle.encrypt( + algorithm, + keyPair.publicKey, + data + ); + + const decrypted = await crypto.subtle.decrypt( + algorithm, + keyPair.privateKey, + encrypted + ); + } + + f().then(done); `); }); - it('ignores the presense of crypto in endowments if remapTypedArrays is false', () => { + it('ignores the presense of crypto in endowments if remapTypedArrays is false', (done) => { const env = createVirtualEnvironment(window, { - endowments: Object.getOwnPropertyDescriptors({ Crypto, crypto, expect }), + endowments: Object.getOwnPropertyDescriptors({ Crypto, crypto, done, expect }), remapTypedArrays: false, }); @@ -76,6 +168,34 @@ describe('Crypto', () => { expect(() => { crypto.getRandomValues(new Uint8Array(1)); }).not.toThrow(); + async function f() { + const algorithm = { name: "RSA-OAEP" }; + const data = new Uint8Array([255]); + const keyPair = await window.crypto.subtle.generateKey( + { + name: "RSA-OAEP", + modulusLength: 4096, + publicExponent: new Uint8Array([1, 0, 1]), + hash: "SHA-256", + }, + true, + ["encrypt", "decrypt"], + ); + + const encrypted = await crypto.subtle.encrypt( + algorithm, + keyPair.publicKey, + data + ); + + const decrypted = await crypto.subtle.decrypt( + algorithm, + keyPair.privateKey, + encrypted + ); + } + + f().then(done); `); }); }); @@ -86,6 +206,18 @@ describe('DataView', () => { endowments: Object.getOwnPropertyDescriptors({ expect }), }); + env.evaluate(` + const buffer = new ArrayBuffer(8); + const dataView = new DataView(buffer); + expect(dataView[0]).toBe(undefined); + `); + }); + it('should not support index access, when typed arrays are not remapped', () => { + const env = createVirtualEnvironment(window, { + endowments: Object.getOwnPropertyDescriptors({ expect }), + remapTypedArrays: false, + }); + env.evaluate(` const buffer = new ArrayBuffer(8); const dataView = new DataView(buffer); @@ -100,6 +232,25 @@ describe('FileReader', () => { endowments: Object.getOwnPropertyDescriptors({ done, expect }), }); + env.evaluate(` + const source = new Uint8Array([97, 98, 99]); + const blob = new Blob([source]); + const reader = new FileReader(); + + reader.onload = (event) => { + expect(reader.result.byteLength).toBe(source.length); + expect(reader.result).toBeInstanceOf(ArrayBuffer); + done(); + }; + reader.readAsArrayBuffer(blob); + `); + }); + it('reads from blobs created from typed arrays, when typed arrays are not remapped', (done) => { + const env = createVirtualEnvironment(window, { + endowments: Object.getOwnPropertyDescriptors({ done, expect }), + remapTypedArrays: false, + }); + env.evaluate(` const source = new Uint8Array([97, 98, 99]); const blob = new Blob([source]); @@ -146,6 +297,37 @@ describe('TypedArray', () => { } `); }); + it('should support in bound index access, when typed arrays are not remapped', () => { + const env = createVirtualEnvironment(window, { + endowments: Object.getOwnPropertyDescriptors({ expect }), + remapTypedArrays: false, + }); + + env.evaluate(` + const buffer = new ArrayBuffer(8); + const bigIntTypedArrays = [ + new BigInt64Array(buffer), + new BigUint64Array(buffer), + ]; + for (const bigIntTypedArray of bigIntTypedArrays) { + expect(typeof bigIntTypedArray[0]).toBe('bigint'); + } + const typedArrays = [ + new Float32Array(buffer), + new Float64Array(buffer), + new Int8Array(buffer), + new Int16Array(buffer), + new Int32Array(buffer), + new Uint8Array(buffer), + new Uint8ClampedArray(buffer), + new Uint16Array(buffer), + new Uint32Array(buffer), + ]; + for (const typedArray of typedArrays) { + expect(typeof typedArray[0]).toBe('number'); + } + `); + }); it('should support in bound index access with modified prototypes', () => { const env = createVirtualEnvironment(window, { endowments: Object.getOwnPropertyDescriptors({ expect }), @@ -172,6 +354,33 @@ describe('TypedArray', () => { } `); }); + it('should support in bound index access with modified prototypes, when typed arrays are not remapped', () => { + const env = createVirtualEnvironment(window, { + endowments: Object.getOwnPropertyDescriptors({ expect }), + remapTypedArrays: false, + }); + + env.evaluate(` + const buffer = new ArrayBuffer(8); + const typedArrays = [ + new BigInt64Array(buffer), + new BigUint64Array(buffer), + new Float32Array(buffer), + new Float64Array(buffer), + new Int8Array(buffer), + new Int16Array(buffer), + new Int32Array(buffer), + new Uint8Array(buffer), + new Uint8ClampedArray(buffer), + new Uint16Array(buffer), + new Uint32Array(buffer), + ]; + for (const typedArray of typedArrays) { + Reflect.setPrototypeOf(typedArray, { length: 0 }); + expect(typedArray[0]).toBeDefined(); + } + `); + }); it('should support setting in bound index values on subclasses', () => { const env = createVirtualEnvironment(window, { endowments: Object.getOwnPropertyDescriptors({ expect }), @@ -204,6 +413,39 @@ describe('TypedArray', () => { } `); }); + it('should support setting in bound index values on subclasses, when typed arrays are not remapped', () => { + const env = createVirtualEnvironment(window, { + endowments: Object.getOwnPropertyDescriptors({ expect }), + remapTypedArrays: false, + }); + + env.evaluate(` + const buffer = new ArrayBuffer(8); + const Ctors = [ + BigInt64Array, + BigUint64Array, + Float32Array, + Float64Array, + Int8Array, + Int16Array, + Int32Array, + Uint8Array, + Uint8ClampedArray, + Uint16Array, + Uint32Array, + ]; + for (const Ctor of Ctors) { + class Subclass extends Ctor { + constructor(arrayBuffer) { + super(arrayBuffer); + this[0] = this[0]; + } + } + const subclassed = new Subclass(buffer); + expect(subclassed[0]).toBeDefined(); + } + `); + }); it('should treat out of bound index access as undefined', () => { const env = createVirtualEnvironment(window, { endowments: Object.getOwnPropertyDescriptors({ expect }), @@ -230,11 +472,64 @@ describe('TypedArray', () => { } `); }); + it('should treat out of bound index access as undefined, when typed arrays are not remapped', () => { + const env = createVirtualEnvironment(window, { + endowments: Object.getOwnPropertyDescriptors({ expect }), + remapTypedArrays: false, + }); + + env.evaluate(` + const buffer = new ArrayBuffer(8); + const typedArrays = [ + new BigInt64Array(buffer), + new BigUint64Array(buffer), + new Float32Array(buffer), + new Float64Array(buffer), + new Int8Array(buffer), + new Int16Array(buffer), + new Int32Array(buffer), + new Uint8Array(buffer), + new Uint8ClampedArray(buffer), + new Uint16Array(buffer), + new Uint32Array(buffer), + ]; + for (const typedArray of typedArrays) { + expect(typedArray[-1]).toBe(undefined); + expect(typedArray[1000]).toBe(undefined); + } + `); + }); it('should support subarray method', () => { const env = createVirtualEnvironment(window, { endowments: Object.getOwnPropertyDescriptors({ expect }), }); + env.evaluate(` + const buffer = new ArrayBuffer(8); + const typedArrays = [ + new BigInt64Array(buffer), + new BigUint64Array(buffer), + new Float32Array(buffer), + new Float64Array(buffer), + new Int8Array(buffer), + new Int16Array(buffer), + new Int32Array(buffer), + new Uint8Array(buffer), + new Uint8ClampedArray(buffer), + new Uint16Array(buffer), + new Uint32Array(buffer), + ]; + for (const typedArray of typedArrays) { + expect(() => typedArray.subarray(0)).not.toThrow(); + } + `); + }); + it('should support subarray method, when typed arrays are not remapped', () => { + const env = createVirtualEnvironment(window, { + endowments: Object.getOwnPropertyDescriptors({ expect }), + remapTypedArrays: false, + }); + env.evaluate(` const buffer = new ArrayBuffer(8); const typedArrays = [