From da48985b025ce6b3fb97b0d48f49256ac54f5b95 Mon Sep 17 00:00:00 2001 From: david0xd Date: Thu, 8 Dec 2022 18:24:40 +0100 Subject: [PATCH] Harden endowments Update AVA config Add harden for default endowments and test Add some refactoring and fix coverage issues Add endowment registry Add hardening for special endowment cases (snap & ethereum) Refactor nyc config Revert hardening of the ethereum endowment Update ava test runner config Revert default-endowments.ts Additionally harden args and returned values Add script for updating coverage thresholds Refactor tests related to hardening of the endowments (optimization) Update coverage thresholds after refactoring Add tests for endowment modules Add object walker utility Integrate object-walker into the AVA security tests and do some refactoring Revert hardening of a snap endowment in index.ts (for now) Manually resolve coverage threshold confusion after deleting line of code --- .../snaps-execution-environments/.c8rc.json | 2 +- .../ava.config.js | 2 +- .../jest.config.js | 18 +- .../jest.environment.js | 1 + .../nyc.config.js | 10 + .../snaps-execution-environments/package.json | 6 +- .../src/common/endowments/abortController.ts | 16 ++ .../src/common/endowments/abortSignal.ts | 16 ++ .../src/common/endowments/arrayBuffer.ts | 16 ++ .../src/common/endowments/atob.ts | 16 ++ .../src/common/endowments/bigInt.ts | 16 ++ .../src/common/endowments/bigInt64Array.ts | 16 ++ .../src/common/endowments/bigUint64Array.ts | 16 ++ .../src/common/endowments/btoa.ts | 16 ++ .../src/common/endowments/crypto.ts | 5 +- .../src/common/endowments/dataView.ts | 16 ++ .../endowments/endowmentModules.ava.test.ts | 230 +++++++++++++++ .../common/endowments/endowmentRegistry.ts | 62 ++++ .../src/common/endowments/float32Array.ts | 16 ++ .../src/common/endowments/float64Array.ts | 16 ++ .../endowments/hardenedEndowments.ava.test.ts | 264 ++++++++++++++++++ .../src/common/endowments/index.ts | 21 +- .../src/common/endowments/int16Array.ts | 16 ++ .../src/common/endowments/int32Array.ts | 16 ++ .../src/common/endowments/int8Array.ts | 16 ++ .../src/common/endowments/interval.ts | 8 +- .../src/common/endowments/math.ts | 4 +- .../src/common/endowments/network.ts | 16 +- .../common/endowments/security-utils/index.ts | 5 + .../security-utils/object-walker.test.ts | 182 ++++++++++++ .../security-utils/object-walker.ts | 95 +++++++ .../src/common/endowments/textDecoder.ts | 16 ++ .../src/common/endowments/textEncoder.ts | 16 ++ .../src/common/endowments/timeout.ava.test.ts | 19 -- .../src/common/endowments/timeout.ts | 8 +- .../src/common/endowments/uint16Array.ts | 16 ++ .../src/common/endowments/uint32Array.ts | 16 ++ .../common/endowments/uint8ClampedArray.ts | 16 ++ .../src/common/endowments/uint8array.ts | 16 ++ .../src/common/endowments/url.ts | 16 ++ .../src/common/endowments/webAssembly.ts | 16 ++ .../update-coverage-thresholds.js | 76 +++++ 42 files changed, 1321 insertions(+), 65 deletions(-) create mode 100644 packages/snaps-execution-environments/nyc.config.js create mode 100644 packages/snaps-execution-environments/src/common/endowments/abortController.ts create mode 100644 packages/snaps-execution-environments/src/common/endowments/abortSignal.ts create mode 100644 packages/snaps-execution-environments/src/common/endowments/arrayBuffer.ts create mode 100644 packages/snaps-execution-environments/src/common/endowments/atob.ts create mode 100644 packages/snaps-execution-environments/src/common/endowments/bigInt.ts create mode 100644 packages/snaps-execution-environments/src/common/endowments/bigInt64Array.ts create mode 100644 packages/snaps-execution-environments/src/common/endowments/bigUint64Array.ts create mode 100644 packages/snaps-execution-environments/src/common/endowments/btoa.ts create mode 100644 packages/snaps-execution-environments/src/common/endowments/dataView.ts create mode 100644 packages/snaps-execution-environments/src/common/endowments/endowmentModules.ava.test.ts create mode 100644 packages/snaps-execution-environments/src/common/endowments/endowmentRegistry.ts create mode 100644 packages/snaps-execution-environments/src/common/endowments/float32Array.ts create mode 100644 packages/snaps-execution-environments/src/common/endowments/float64Array.ts create mode 100644 packages/snaps-execution-environments/src/common/endowments/hardenedEndowments.ava.test.ts create mode 100644 packages/snaps-execution-environments/src/common/endowments/int16Array.ts create mode 100644 packages/snaps-execution-environments/src/common/endowments/int32Array.ts create mode 100644 packages/snaps-execution-environments/src/common/endowments/int8Array.ts create mode 100644 packages/snaps-execution-environments/src/common/endowments/security-utils/index.ts create mode 100644 packages/snaps-execution-environments/src/common/endowments/security-utils/object-walker.test.ts create mode 100644 packages/snaps-execution-environments/src/common/endowments/security-utils/object-walker.ts create mode 100644 packages/snaps-execution-environments/src/common/endowments/textDecoder.ts create mode 100644 packages/snaps-execution-environments/src/common/endowments/textEncoder.ts delete mode 100644 packages/snaps-execution-environments/src/common/endowments/timeout.ava.test.ts create mode 100644 packages/snaps-execution-environments/src/common/endowments/uint16Array.ts create mode 100644 packages/snaps-execution-environments/src/common/endowments/uint32Array.ts create mode 100644 packages/snaps-execution-environments/src/common/endowments/uint8ClampedArray.ts create mode 100644 packages/snaps-execution-environments/src/common/endowments/uint8array.ts create mode 100644 packages/snaps-execution-environments/src/common/endowments/url.ts create mode 100644 packages/snaps-execution-environments/src/common/endowments/webAssembly.ts create mode 100644 packages/snaps-execution-environments/update-coverage-thresholds.js diff --git a/packages/snaps-execution-environments/.c8rc.json b/packages/snaps-execution-environments/.c8rc.json index 0337876aaa..032637ed03 100644 --- a/packages/snaps-execution-environments/.c8rc.json +++ b/packages/snaps-execution-environments/.c8rc.json @@ -1,5 +1,5 @@ { - "reporter": ["html", "json-summary", "text", "json"], + "reporter": ["html", "json-summary", "json"], "exclude": ["*.js", "./src/index.ts", "**/*.ava.test.ts"], "report-dir": "./coverage-ava" } diff --git a/packages/snaps-execution-environments/ava.config.js b/packages/snaps-execution-environments/ava.config.js index bd26f77592..b341f9139e 100644 --- a/packages/snaps-execution-environments/ava.config.js +++ b/packages/snaps-execution-environments/ava.config.js @@ -1,9 +1,9 @@ module.exports = () => { return { - concurrency: 5, extensions: ['ts'], require: ['ts-node/register'], verbose: true, files: ['src/**/*.ava.test.ts'], + timeout: '30s', }; }; diff --git a/packages/snaps-execution-environments/jest.config.js b/packages/snaps-execution-environments/jest.config.js index 85daac7887..759caad624 100644 --- a/packages/snaps-execution-environments/jest.config.js +++ b/packages/snaps-execution-environments/jest.config.js @@ -2,20 +2,20 @@ const deepmerge = require('deepmerge'); const baseConfig = require('../../jest.config.base'); +delete baseConfig.coverageThreshold; + module.exports = deepmerge(baseConfig, { - coveragePathIgnorePatterns: ['./src/index.ts', '.ava.test.ts'], - coverageThreshold: { - global: { - branches: 83.93, - functions: 92.25, - lines: 87.07, - statements: 87.18, - }, - }, + coveragePathIgnorePatterns: [ + './src/index.ts', + '.ava.test.ts', + 'update-coverage-thresholds.js', + ], testEnvironment: '/jest.environment.js', testEnvironmentOptions: { customExportConditions: ['node', 'node-addons'], }, testTimeout: 2500, testPathIgnorePatterns: ['.ava.test.ts'], + coverageProvider: 'v8', + coverageReporters: ['html', 'json-summary', 'json'], }); diff --git a/packages/snaps-execution-environments/jest.environment.js b/packages/snaps-execution-environments/jest.environment.js index de208e70d8..dd5774b584 100644 --- a/packages/snaps-execution-environments/jest.environment.js +++ b/packages/snaps-execution-environments/jest.environment.js @@ -13,6 +13,7 @@ module.exports = class CustomTestEnvironment extends TestEnvironment { this.global.TextDecoder = TextDecoder; this.global.ArrayBuffer = ArrayBuffer; this.global.Uint8Array = Uint8Array; + this.global.harden = (param) => param; } } }; diff --git a/packages/snaps-execution-environments/nyc.config.js b/packages/snaps-execution-environments/nyc.config.js new file mode 100644 index 0000000000..d66fed6f4c --- /dev/null +++ b/packages/snaps-execution-environments/nyc.config.js @@ -0,0 +1,10 @@ +/** + * NYC coverage reporter configuration. + */ +module.exports = { + 'check-coverage': true, + branches: 91.02, + lines: 91.36, + functions: 92.85, + statements: 91.36, +}; diff --git a/packages/snaps-execution-environments/package.json b/packages/snaps-execution-environments/package.json index 635ee6cb7f..bc85cbe48c 100644 --- a/packages/snaps-execution-environments/package.json +++ b/packages/snaps-execution-environments/package.json @@ -12,10 +12,10 @@ "dist/" ], "scripts": { - "test": "yarn test:ava && jest && yarn posttest && yarn merge:coverage", - "posttest": "jest-it-up --margin 0.25", + "test": "yarn test:ava && jest && yarn merge:coverage && yarn posttest", + "posttest": "node update-coverage-thresholds.js", "test:ava": "c8 ava", - "merge:coverage": "yarn mkdirp coverage-all && shx cp coverage/coverage-final.json coverage-all/coverage-final-jest.json && shx cp coverage-ava/coverage-final.json coverage-all/coverage-final-ava.json && rimraf 'coverage' 'coverage-ava' && nyc merge coverage-all coverage-merged/merged-coverage.json && nyc report -t coverage-merged --report-dir coverage --reporter=html --reporter=json-summary --reporter=json && rimraf 'coverage-merged' 'coverage-all'", + "merge:coverage": "yarn mkdirp coverage-all && shx cp coverage/coverage-final.json coverage-all/coverage-final-jest.json && shx cp coverage-ava/coverage-final.json coverage-all/coverage-final-ava.json && rimraf 'coverage' 'coverage-ava' && nyc merge coverage-all coverage-merged/merged-coverage.json && nyc report -t coverage-merged --report-dir coverage --reporter=text --reporter=html --reporter=json-summary --reporter=json && rimraf 'coverage-merged' 'coverage-all'", "test:ci": "yarn test", "test:watch": "jest --watch", "lint:eslint": "eslint . --cache --ext js,ts", diff --git a/packages/snaps-execution-environments/src/common/endowments/abortController.ts b/packages/snaps-execution-environments/src/common/endowments/abortController.ts new file mode 100644 index 0000000000..ab16d3a45e --- /dev/null +++ b/packages/snaps-execution-environments/src/common/endowments/abortController.ts @@ -0,0 +1,16 @@ +/** + * Creates AbortController function hardened by SES. + * + * @returns An object with the attenuated `AbortController` function. + */ +const createAbortController = () => { + return { + AbortController: harden(AbortController), + } as const; +}; + +const endowmentModule = { + names: ['AbortController'] as const, + factory: createAbortController, +}; +export default endowmentModule; diff --git a/packages/snaps-execution-environments/src/common/endowments/abortSignal.ts b/packages/snaps-execution-environments/src/common/endowments/abortSignal.ts new file mode 100644 index 0000000000..9c50772767 --- /dev/null +++ b/packages/snaps-execution-environments/src/common/endowments/abortSignal.ts @@ -0,0 +1,16 @@ +/** + * Creates AbortSignal function hardened by SES. + * + * @returns An object with the attenuated `AbortSignal` function. + */ +const createAbortSignal = () => { + return { + AbortSignal: harden(AbortSignal), + } as const; +}; + +const endowmentModule = { + names: ['AbortSignal'] as const, + factory: createAbortSignal, +}; +export default endowmentModule; diff --git a/packages/snaps-execution-environments/src/common/endowments/arrayBuffer.ts b/packages/snaps-execution-environments/src/common/endowments/arrayBuffer.ts new file mode 100644 index 0000000000..a705005677 --- /dev/null +++ b/packages/snaps-execution-environments/src/common/endowments/arrayBuffer.ts @@ -0,0 +1,16 @@ +/** + * Creates ArrayBuffer function hardened by SES. + * + * @returns An object with the attenuated `ArrayBuffer` function. + */ +const createArrayBuffer = () => { + return { + ArrayBuffer: harden(ArrayBuffer), + } as const; +}; + +const endowmentModule = { + names: ['ArrayBuffer'] as const, + factory: createArrayBuffer, +}; +export default endowmentModule; diff --git a/packages/snaps-execution-environments/src/common/endowments/atob.ts b/packages/snaps-execution-environments/src/common/endowments/atob.ts new file mode 100644 index 0000000000..d3024020ef --- /dev/null +++ b/packages/snaps-execution-environments/src/common/endowments/atob.ts @@ -0,0 +1,16 @@ +/** + * Creates atob function hardened by SES. + * + * @returns An object with the attenuated `atob` function. + */ +const createAtob = () => { + return { + atob: harden(atob), + } as const; +}; + +const endowmentModule = { + names: ['atob'] as const, + factory: createAtob, +}; +export default endowmentModule; diff --git a/packages/snaps-execution-environments/src/common/endowments/bigInt.ts b/packages/snaps-execution-environments/src/common/endowments/bigInt.ts new file mode 100644 index 0000000000..ea5219c279 --- /dev/null +++ b/packages/snaps-execution-environments/src/common/endowments/bigInt.ts @@ -0,0 +1,16 @@ +/** + * Creates BigInt function hardened by SES. + * + * @returns An object with the attenuated `BigInt` function. + */ +const createBigInt = () => { + return { + BigInt: harden(BigInt), + } as const; +}; + +const endowmentModule = { + names: ['BigInt'] as const, + factory: createBigInt, +}; +export default endowmentModule; diff --git a/packages/snaps-execution-environments/src/common/endowments/bigInt64Array.ts b/packages/snaps-execution-environments/src/common/endowments/bigInt64Array.ts new file mode 100644 index 0000000000..f34be30117 --- /dev/null +++ b/packages/snaps-execution-environments/src/common/endowments/bigInt64Array.ts @@ -0,0 +1,16 @@ +/** + * Creates BigInt64Array function hardened by SES. + * + * @returns An object with the attenuated `BigInt64Array` function. + */ +const createBigInt64Array = () => { + return { + BigInt64Array: harden(BigInt64Array), + } as const; +}; + +const endowmentModule = { + names: ['BigInt64Array'] as const, + factory: createBigInt64Array, +}; +export default endowmentModule; diff --git a/packages/snaps-execution-environments/src/common/endowments/bigUint64Array.ts b/packages/snaps-execution-environments/src/common/endowments/bigUint64Array.ts new file mode 100644 index 0000000000..3771739251 --- /dev/null +++ b/packages/snaps-execution-environments/src/common/endowments/bigUint64Array.ts @@ -0,0 +1,16 @@ +/** + * Creates BigUint64Array function hardened by SES. + * + * @returns An object with the attenuated `BigUint64Array` function. + */ +const createBigUint64Array = () => { + return { + BigUint64Array: harden(BigUint64Array), + } as const; +}; + +const endowmentModule = { + names: ['BigUint64Array'] as const, + factory: createBigUint64Array, +}; +export default endowmentModule; diff --git a/packages/snaps-execution-environments/src/common/endowments/btoa.ts b/packages/snaps-execution-environments/src/common/endowments/btoa.ts new file mode 100644 index 0000000000..84e0d7af65 --- /dev/null +++ b/packages/snaps-execution-environments/src/common/endowments/btoa.ts @@ -0,0 +1,16 @@ +/** + * Creates btoa function hardened by SES. + * + * @returns An object with the attenuated `btoa` function. + */ +const createBtoa = () => { + return { + btoa: harden(btoa), + } as const; +}; + +const endowmentModule = { + names: ['btoa'] as const, + factory: createBtoa, +}; +export default endowmentModule; diff --git a/packages/snaps-execution-environments/src/common/endowments/crypto.ts b/packages/snaps-execution-environments/src/common/endowments/crypto.ts index cc4f498939..7252e61bed 100644 --- a/packages/snaps-execution-environments/src/common/endowments/crypto.ts +++ b/packages/snaps-execution-environments/src/common/endowments/crypto.ts @@ -16,7 +16,10 @@ const createCrypto = () => { // TODO: Figure out if this is enough long-term or if we should use a polyfill. /* eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires, node/global-require */ const crypto = require('crypto').webcrypto; - return { crypto, SubtleCrypto: crypto.subtle.constructor } as const; + return { + crypto: harden(crypto), + SubtleCrypto: harden(crypto.subtle.constructor), + } as const; }; const endowmentModule = { diff --git a/packages/snaps-execution-environments/src/common/endowments/dataView.ts b/packages/snaps-execution-environments/src/common/endowments/dataView.ts new file mode 100644 index 0000000000..b2e3ed3260 --- /dev/null +++ b/packages/snaps-execution-environments/src/common/endowments/dataView.ts @@ -0,0 +1,16 @@ +/** + * Creates DataView function hardened by SES. + * + * @returns An object with the attenuated `DataView` function. + */ +const createDataView = () => { + return { + DataView: harden(DataView), + } as const; +}; + +const endowmentModule = { + names: ['DataView'] as const, + factory: createDataView, +}; +export default endowmentModule; diff --git a/packages/snaps-execution-environments/src/common/endowments/endowmentModules.ava.test.ts b/packages/snaps-execution-environments/src/common/endowments/endowmentModules.ava.test.ts new file mode 100644 index 0000000000..4e65b01e6a --- /dev/null +++ b/packages/snaps-execution-environments/src/common/endowments/endowmentModules.ava.test.ts @@ -0,0 +1,230 @@ +// eslint-disable-next-line import/no-unassigned-import +import 'ses'; +import test from 'ava'; +// FinalizationRegistry will fix type errors in tests related to network endowment. +// eslint-disable-next-line import/no-extraneous-dependencies, @typescript-eslint/no-unused-vars +import FinalizationRegistry from 'globals'; + +import abortController from './abortController'; +import abortSignal from './abortSignal'; +import arrayBuffer from './arrayBuffer'; +import atobEndowment from './atob'; +import bigInt from './bigInt'; +import bigInt64Array from './bigInt64Array'; +import bigUint64Array from './bigUint64Array'; +import btoaEndowment from './btoa'; +import crypto from './crypto'; +import dataView from './dataView'; +import float32Array from './float32Array'; +import float64Array from './float64Array'; +import int16Array from './int16Array'; +import int32Array from './int32Array'; +import int8Array from './int8Array'; +import interval from './interval'; +import math from './math'; +import network from './network'; +import textDecoder from './textDecoder'; +import textEncoder from './textEncoder'; +import timeout from './timeout'; +import uint16Array from './uint16Array'; +import uint32Array from './uint32Array'; +import uint8array from './uint8array'; +import uint8ClampedArray from './uint8ClampedArray'; +import url from './url'; +import webAssembly from './webAssembly'; + +// Note: harden is only defined after calling lockdown +lockdown({ + domainTaming: 'unsafe', + errorTaming: 'unsafe', + stackFiltering: 'verbose', +}); + +test(`AbortController endowment module has expected properties`, (expect) => { + const { names, factory } = abortController; + + expect.deepEqual(names, ['AbortController']); + expect.is(typeof factory, 'function'); +}); + +test(`AbortSignal endowment module has expected properties`, (expect) => { + const { names, factory } = abortSignal; + + expect.deepEqual(names, ['AbortSignal']); + expect.is(typeof factory, 'function'); +}); + +test(`ArrayBuffer endowment module has expected properties`, (expect) => { + const { names, factory } = arrayBuffer; + + expect.deepEqual(names, ['ArrayBuffer']); + expect.is(typeof factory, 'function'); +}); + +test(`atob endowment module has expected properties`, (expect) => { + const { names, factory } = atobEndowment; + + expect.deepEqual(names, ['atob']); + expect.is(typeof factory, 'function'); +}); + +test(`BigInt endowment module has expected properties`, (expect) => { + const { names, factory } = bigInt; + + expect.deepEqual(names, ['BigInt']); + expect.is(typeof factory, 'function'); +}); + +test(`BigInt64Array endowment module has expected properties`, (expect) => { + const { names, factory } = bigInt64Array; + + expect.deepEqual(names, ['BigInt64Array']); + expect.is(typeof factory, 'function'); +}); + +test(`BigUint64Array endowment module has expected properties`, (expect) => { + const { names, factory } = bigUint64Array; + + expect.deepEqual(names, ['BigUint64Array']); + expect.is(typeof factory, 'function'); +}); + +test(`btoa endowment module has expected properties`, (expect) => { + const { names, factory } = btoaEndowment; + + expect.deepEqual(names, ['btoa']); + expect.is(typeof factory, 'function'); +}); + +test(`crypto endowment module has expected properties`, (expect) => { + const { names, factory } = crypto; + + expect.deepEqual(names, ['crypto', 'SubtleCrypto']); + expect.is(typeof factory, 'function'); +}); + +test(`DataView endowment module has expected properties`, (expect) => { + const { names, factory } = dataView; + + expect.deepEqual(names, ['DataView']); + expect.is(typeof factory, 'function'); +}); + +test(`Float32Array endowment module has expected properties`, (expect) => { + const { names, factory } = float32Array; + + expect.deepEqual(names, ['Float32Array']); + expect.is(typeof factory, 'function'); +}); + +test(`Float64Array endowment module has expected properties`, (expect) => { + const { names, factory } = float64Array; + + expect.deepEqual(names, ['Float64Array']); + expect.is(typeof factory, 'function'); +}); + +test(`Int8Array endowment module has expected properties`, (expect) => { + const { names, factory } = int8Array; + + expect.deepEqual(names, ['Int8Array']); + expect.is(typeof factory, 'function'); +}); + +test(`Int16Array endowment module has expected properties`, (expect) => { + const { names, factory } = int16Array; + + expect.deepEqual(names, ['Int16Array']); + expect.is(typeof factory, 'function'); +}); + +test(`Int32Array endowment module has expected properties`, (expect) => { + const { names, factory } = int32Array; + + expect.deepEqual(names, ['Int32Array']); + expect.is(typeof factory, 'function'); +}); + +test(`Interval endowment module has expected properties`, (expect) => { + const { names, factory } = interval; + + expect.deepEqual(names, ['setInterval', 'clearInterval']); + expect.is(typeof factory, 'function'); +}); + +test(`Math endowment module has expected properties`, (expect) => { + const { names, factory } = math; + + expect.deepEqual(names, ['Math']); + expect.is(typeof factory, 'function'); +}); + +test(`Network endowment module has expected properties`, (expect) => { + const { names, factory } = network; + + expect.deepEqual(names, ['fetch', 'WebSocket']); + expect.is(typeof factory, 'function'); +}); + +test(`TextDecoder endowment module has expected properties`, (expect) => { + const { names, factory } = textDecoder; + + expect.deepEqual(names, ['TextDecoder']); + expect.is(typeof factory, 'function'); +}); + +test(`TextEncoder endowment module has expected properties`, (expect) => { + const { names, factory } = textEncoder; + + expect.deepEqual(names, ['TextEncoder']); + expect.is(typeof factory, 'function'); +}); + +test(`Timeout endowment module has expected properties`, (expect) => { + const { names, factory } = timeout; + + expect.deepEqual(names, ['setTimeout', 'clearTimeout']); + expect.is(typeof factory, 'function'); +}); + +test(`Uint8Array endowment module has expected properties`, (expect) => { + const { names, factory } = uint8array; + + expect.deepEqual(names, ['Uint8Array']); + expect.is(typeof factory, 'function'); +}); + +test(`Uint8ClampedArray endowment module has expected properties`, (expect) => { + const { names, factory } = uint8ClampedArray; + + expect.deepEqual(names, ['Uint8ClampedArray']); + expect.is(typeof factory, 'function'); +}); + +test(`Uint16Array endowment module has expected properties`, (expect) => { + const { names, factory } = uint16Array; + + expect.deepEqual(names, ['Uint16Array']); + expect.is(typeof factory, 'function'); +}); + +test(`Uint32Array endowment module has expected properties`, (expect) => { + const { names, factory } = uint32Array; + + expect.deepEqual(names, ['Uint32Array']); + expect.is(typeof factory, 'function'); +}); + +test(`URL endowment module has expected properties`, (expect) => { + const { names, factory } = url; + + expect.deepEqual(names, ['URL']); + expect.is(typeof factory, 'function'); +}); + +test(`WebAssembly endowment module has expected properties`, (expect) => { + const { names, factory } = webAssembly; + + expect.deepEqual(names, ['WebAssembly']); + expect.is(typeof factory, 'function'); +}); diff --git a/packages/snaps-execution-environments/src/common/endowments/endowmentRegistry.ts b/packages/snaps-execution-environments/src/common/endowments/endowmentRegistry.ts new file mode 100644 index 0000000000..826fffdc1d --- /dev/null +++ b/packages/snaps-execution-environments/src/common/endowments/endowmentRegistry.ts @@ -0,0 +1,62 @@ +import abortController from './abortController'; +import abortSignal from './abortSignal'; +import arrayBuffer from './arrayBuffer'; +import atobEndowment from './atob'; +import bigInt from './bigInt'; +import bigInt64Array from './bigInt64Array'; +import bigUint64Array from './bigUint64Array'; +import btoaEndowment from './btoa'; +import crypto from './crypto'; +import dataView from './dataView'; +import float32Array from './float32Array'; +import float64Array from './float64Array'; +import int16Array from './int16Array'; +import int32Array from './int32Array'; +import int8Array from './int8Array'; +import interval from './interval'; +import math from './math'; +import network from './network'; +import textDecoder from './textDecoder'; +import textEncoder from './textEncoder'; +import timeout from './timeout'; +import uint16Array from './uint16Array'; +import uint32Array from './uint32Array'; +import uint8array from './uint8array'; +import uint8ClampedArray from './uint8ClampedArray'; +import url from './url'; +import webAssembly from './webAssembly'; + +/** + * Registry of all attenuated and/or hardened endowments. + */ +const registeredEndowments = [ + atobEndowment, + btoaEndowment, + bigInt, + crypto, + math, + timeout, + textDecoder, + textEncoder, + url, + webAssembly, + interval, + int8Array, + uint8array, + uint8ClampedArray, + int16Array, + uint16Array, + int32Array, + uint32Array, + float32Array, + float64Array, + bigInt64Array, + bigUint64Array, + dataView, + arrayBuffer, + abortController, + abortSignal, + network, +]; + +export default registeredEndowments; diff --git a/packages/snaps-execution-environments/src/common/endowments/float32Array.ts b/packages/snaps-execution-environments/src/common/endowments/float32Array.ts new file mode 100644 index 0000000000..2f2c43d0fb --- /dev/null +++ b/packages/snaps-execution-environments/src/common/endowments/float32Array.ts @@ -0,0 +1,16 @@ +/** + * Creates Float32Array function hardened by SES. + * + * @returns An object with the attenuated `Float32Array` function. + */ +const createFloat32Array = () => { + return { + Float32Array: harden(Float32Array), + } as const; +}; + +const endowmentModule = { + names: ['Float32Array'] as const, + factory: createFloat32Array, +}; +export default endowmentModule; diff --git a/packages/snaps-execution-environments/src/common/endowments/float64Array.ts b/packages/snaps-execution-environments/src/common/endowments/float64Array.ts new file mode 100644 index 0000000000..87e327e40a --- /dev/null +++ b/packages/snaps-execution-environments/src/common/endowments/float64Array.ts @@ -0,0 +1,16 @@ +/** + * Creates Float64Array function hardened by SES. + * + * @returns An object with the attenuated `Float64Array` function. + */ +const createFloat64Array = () => { + return { + Float64Array: harden(Float64Array), + } as const; +}; + +const endowmentModule = { + names: ['Float64Array'] as const, + factory: createFloat64Array, +}; +export default endowmentModule; diff --git a/packages/snaps-execution-environments/src/common/endowments/hardenedEndowments.ava.test.ts b/packages/snaps-execution-environments/src/common/endowments/hardenedEndowments.ava.test.ts new file mode 100644 index 0000000000..b6b3da3d63 --- /dev/null +++ b/packages/snaps-execution-environments/src/common/endowments/hardenedEndowments.ava.test.ts @@ -0,0 +1,264 @@ +// eslint-disable-next-line import/no-unassigned-import +import 'ses'; +import test from 'ava'; +// FinalizationRegistry will fix type errors in tests related to network endowment. +// eslint-disable-next-line import/no-extraneous-dependencies, @typescript-eslint/no-unused-vars +import FinalizationRegistry from 'globals'; + +import Crypto from './crypto'; +import registeredEndowments from './endowmentRegistry'; +import interval from './interval'; +import math from './math'; +import network from './network'; +import { walkPropertiesAndSearchForReference } from './security-utils'; +import timeout from './timeout'; + +const globalThis = global; + +// Note: harden is only defined after calling lockdown +lockdown({ + domainTaming: 'unsafe', + errorTaming: 'unsafe', + stackFiltering: 'verbose', +}); + +// Call factory method for each endowment. It will harden each of them. +// This is how endowments are created (see index.ts / createEndowments()). +registeredEndowments.forEach((endowment) => endowment.factory()); + +// Specially attenuated endowments or endowments that require +// to be imported in a different way +const { SubtleCrypto, crypto } = Crypto.factory(); +const { + setTimeout: setTimeoutAttenuated, + clearTimeout: clearTimeoutAttenuated, +} = timeout.factory(); +const { + setInterval: setIntervalAttenuated, + clearInterval: clearIntervalAttenuated, +} = interval.factory(); +const { Math: mathAttenuated } = math.factory(); +const { fetch: fetchAttenuated, WebSocket: WebSocketAttenuated } = + network.factory(); + +// All the endowments to be tested +const testSubjects = { + // --- Constructor functions + BigInt: { + endowments: { BigInt }, + factory: () => BigInt(3), + }, + SubtleCrypto: { + endowments: { SubtleCrypto }, + factory: () => undefined, + }, + TextDecoder: { + endowments: { TextDecoder }, + factory: () => new TextDecoder(), + }, + TextEncoder: { + endowments: { TextEncoder }, + factory: () => new TextEncoder(), + }, + URL: { + endowments: { URL }, + factory: () => new URL('https://metamask.io/snaps/'), + }, + Int8Array: { + endowments: { Int8Array }, + factory: () => new Int8Array(), + }, + Uint8Array: { + endowments: { Uint8Array }, + factory: () => new Uint8Array(), + }, + Uint8ClampedArray: { + endowments: { Uint8ClampedArray }, + factory: () => new Uint8ClampedArray(), + }, + Int16Array: { + endowments: { Int16Array }, + factory: () => new Int16Array(), + }, + Uint16Array: { + endowments: { Uint16Array }, + factory: () => new Uint16Array(), + }, + Int32Array: { + endowments: { Int32Array }, + factory: () => new Int32Array(), + }, + Uint32Array: { + endowments: { Uint32Array }, + factory: () => new Uint32Array(), + }, + Float32Array: { + endowments: { Float32Array }, + factory: () => new Float32Array(), + }, + Float64Array: { + endowments: { Float64Array }, + factory: () => new Float64Array(), + }, + BigInt64Array: { + endowments: { BigInt64Array }, + factory: () => new BigInt64Array(), + }, + BigUint64Array: { + endowments: { BigUint64Array }, + factory: () => new BigUint64Array(), + }, + DataView: { + endowments: { DataView, ArrayBuffer }, + factory: () => new DataView(new ArrayBuffer(64)), + }, + ArrayBuffer: { + endowments: { ArrayBuffer }, + factory: () => new ArrayBuffer(64), + }, + AbortController: { + endowments: { AbortController }, + factory: () => new AbortController(), + }, + AbortSignal: { + endowments: { AbortSignal }, + // @ts-expect-error This is not ok, but will provide access to its prototype + factory: () => AbortSignal.abort(), + }, + WebSocketAttenuated: { + endowments: { WebSocketAttenuated }, + factory: () => undefined, + }, + // --- Objects + console: { + endowments: { console }, + factory: () => console, + }, + crypto: { + endowments: { crypto }, + factory: () => crypto, + }, + mathAttenuated: { + endowments: { mathAttenuated }, + factory: () => mathAttenuated, + }, + WebAssembly: { + endowments: { WebAssembly }, + factory: () => WebAssembly, + }, + // --- Functions + atob: { + endowments: { atob }, + factory: () => atob('U25hcHM='), + }, + btoa: { + endowments: { btoa }, + factory: () => btoa('Snaps'), + }, + setTimeoutAttenuated: { + endowments: { setTimeoutAttenuated }, + factory: () => setTimeoutAttenuated((param: unknown) => param, 1), + }, + clearTimeoutAttenuated: { + endowments: { clearTimeoutAttenuated }, + factory: () => undefined, + }, + setIntervalAttenuated: { + endowments: { setIntervalAttenuated }, + factory: () => setIntervalAttenuated((param: unknown) => param, 100000), + }, + clearIntervalAttenuated: { + endowments: { clearIntervalAttenuated }, + factory: () => undefined, + }, + fetchAttenuated: { + endowments: { fetchAttenuated }, + factory: () => undefined, + }, +}; + +// These are fake types just to make this test work with the TypeScript +type HardenedEndowmentSubject = { + // eslint-disable-next-line @typescript-eslint/naming-convention + __flag: unknown; + // eslint-disable-next-line @typescript-eslint/naming-convention + prototype: { __flag: unknown }; +}; +type HardenedEndowmentInstance = { + // eslint-disable-next-line @typescript-eslint/naming-convention + __flag: unknown; + // eslint-disable-next-line @typescript-eslint/naming-convention + __proto__: { __flag: unknown }; +}; + +/** + * Test helper function. + * + * @param subject - Test subject (instance, object, function). + * @param factory - Factory that creates an instance using constructor function. + * @returns Array of error messages. + */ +function code( + subject: HardenedEndowmentSubject, + factory: () => HardenedEndowmentInstance, +): unknown[] { + const log = []; + const instance = factory(); + try { + subject.__flag = 'not_secure'; + } catch (error) { + log.push(error.message); + } + try { + if (instance) { + instance.__flag = 'not_secure'; + } + } catch (error) { + log.push(error.message); + } + try { + subject.prototype.__flag = 'not_secure'; + } catch (error) { + log.push(error.message); + } + try { + if (instance) { + // eslint-disable-next-line @typescript-eslint/naming-convention + Object.setPrototypeOf(instance, { __flag: 'not_secure' }); + } + } catch (error) { + log.push(error.message); + } + return log; +} + +Object.entries(testSubjects).forEach(([name, { endowments, factory }]) => { + test(`hardening protects ${name}`, (expect) => { + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + const source = `;(${code})(${name},${factory})`; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const subject = endowments[name]; + const c1 = new Compartment(endowments, {}, {}); + const errors = c1.evaluate(source); + const instance = factory(); + + expect.falsy(subject.__flag, 'flag is leaking via endowed object'); + if (instance) { + expect.falsy(instance.__flag, 'flag is leaking via prototype'); + } + expect.assert(errors.length > 0); + }); + + if (factory()) { + test(`endowment ${name} does not leak global this`, (expect) => { + const instance = factory(); + const searchResult = walkPropertiesAndSearchForReference( + instance, + globalThis, + ); + + expect.is(searchResult, false); + }); + } +}); diff --git a/packages/snaps-execution-environments/src/common/endowments/index.ts b/packages/snaps-execution-environments/src/common/endowments/index.ts index faf49ecb39..09d5df63ce 100644 --- a/packages/snaps-execution-environments/src/common/endowments/index.ts +++ b/packages/snaps-execution-environments/src/common/endowments/index.ts @@ -3,11 +3,7 @@ import { SnapsGlobalObject } from '@metamask/snaps-utils'; import { hasProperty } from '@metamask/utils'; import { rootRealmGlobal } from '../globalObject'; -import crypto from './crypto'; -import interval from './interval'; -import math from './math'; -import network from './network'; -import timeout from './timeout'; +import registeredEndowments from './endowmentRegistry'; type EndowmentFactoryResult = { /** @@ -27,15 +23,12 @@ type EndowmentFactoryResult = { * the same factory function, but we only call each factory once for each snap. * See {@link createEndowments} for details. */ -const endowmentFactories = [timeout, interval, network, crypto, math].reduce( - (factories, builder) => { - builder.names.forEach((name) => { - factories.set(name, builder.factory); - }); - return factories; - }, - new Map EndowmentFactoryResult>(), -); +const endowmentFactories = registeredEndowments.reduce((factories, builder) => { + builder.names.forEach((name) => { + factories.set(name, builder.factory); + }); + return factories; +}, new Map EndowmentFactoryResult>()); /** * Gets the endowments for a particular Snap. Some endowments, like `setTimeout` diff --git a/packages/snaps-execution-environments/src/common/endowments/int16Array.ts b/packages/snaps-execution-environments/src/common/endowments/int16Array.ts new file mode 100644 index 0000000000..3cc05dca56 --- /dev/null +++ b/packages/snaps-execution-environments/src/common/endowments/int16Array.ts @@ -0,0 +1,16 @@ +/** + * Creates Int16Array function hardened by SES. + * + * @returns An object with the attenuated `Int16Array` function. + */ +const createInt16Array = () => { + return { + Int16Array: harden(Int16Array), + } as const; +}; + +const endowmentModule = { + names: ['Int16Array'] as const, + factory: createInt16Array, +}; +export default endowmentModule; diff --git a/packages/snaps-execution-environments/src/common/endowments/int32Array.ts b/packages/snaps-execution-environments/src/common/endowments/int32Array.ts new file mode 100644 index 0000000000..009f5fed4b --- /dev/null +++ b/packages/snaps-execution-environments/src/common/endowments/int32Array.ts @@ -0,0 +1,16 @@ +/** + * Creates Int32Array function hardened by SES. + * + * @returns An object with the attenuated `Int32Array` function. + */ +const createInt32Array = () => { + return { + Int32Array: harden(Int32Array), + } as const; +}; + +const endowmentModule = { + names: ['Int32Array'] as const, + factory: createInt32Array, +}; +export default endowmentModule; diff --git a/packages/snaps-execution-environments/src/common/endowments/int8Array.ts b/packages/snaps-execution-environments/src/common/endowments/int8Array.ts new file mode 100644 index 0000000000..824ac60050 --- /dev/null +++ b/packages/snaps-execution-environments/src/common/endowments/int8Array.ts @@ -0,0 +1,16 @@ +/** + * Creates Int8Array function hardened by SES. + * + * @returns An object with the attenuated `Int8Array` function. + */ +const createInt8Array = () => { + return { + Int8Array: harden(Int8Array), + } as const; +}; + +const endowmentModule = { + names: ['Int8Array'] as const, + factory: createInt8Array, +}; +export default endowmentModule; diff --git a/packages/snaps-execution-environments/src/common/endowments/interval.ts b/packages/snaps-execution-environments/src/common/endowments/interval.ts index bf7753ec6d..76c4b68934 100644 --- a/packages/snaps-execution-environments/src/common/endowments/interval.ts +++ b/packages/snaps-execution-environments/src/common/endowments/interval.ts @@ -17,13 +17,15 @@ const createInterval = () => { `The interval handler must be a function. Received: ${typeof handler}`, ); } + harden(handler); const handle = Object.freeze({}); const platformHandle = setInterval(handler, timeout); registeredHandles.set(handle, platformHandle); - return handle; + return harden(handle); }; const _clearInterval = (handle: unknown): void => { + harden(handle); const platformHandle = registeredHandles.get(handle); if (platformHandle !== undefined) { clearInterval(platformHandle as any); @@ -38,8 +40,8 @@ const createInterval = () => { }; return { - setInterval: _setInterval, - clearInterval: _clearInterval, + setInterval: harden(_setInterval), + clearInterval: harden(_clearInterval), teardownFunction, } as const; }; diff --git a/packages/snaps-execution-environments/src/common/endowments/math.ts b/packages/snaps-execution-environments/src/common/endowments/math.ts index 942d123133..d21446af8c 100644 --- a/packages/snaps-execution-environments/src/common/endowments/math.ts +++ b/packages/snaps-execution-environments/src/common/endowments/math.ts @@ -22,7 +22,7 @@ function createMath() { return { ...target, [key]: rootRealmGlobal.Math[key] }; }, {}); - return { + return harden({ Math: { ...math, random: () => { @@ -45,7 +45,7 @@ function createMath() { return crypto.getRandomValues(new Uint32Array(1))[0] / 2 ** 32; }, }, - }; + }); } const endowmentModule = { diff --git a/packages/snaps-execution-environments/src/common/endowments/network.ts b/packages/snaps-execution-environments/src/common/endowments/network.ts index 448a23e358..10df5b83e0 100644 --- a/packages/snaps-execution-environments/src/common/endowments/network.ts +++ b/packages/snaps-execution-environments/src/common/endowments/network.ts @@ -174,7 +174,7 @@ const createNetwork = () => { () => openConnections.delete(openBodyConnection), ); } - return res; + return harden(res); }; /** @@ -211,7 +211,7 @@ const createNetwork = () => { } get onclose(): WebSocketCallback | null { - return this.#oncloseOriginal; + return harden(this.#oncloseOriginal); } set onclose(callback: WebSocketCallback | null) { @@ -222,7 +222,7 @@ const createNetwork = () => { } get onerror(): ((this: WebSocket, ev: Event) => any) | null { - return this.#onerrorOriginal; + return harden(this.#onerrorOriginal); } set onerror(callback: ((this: WebSocket, ev: Event) => any) | null) { @@ -231,7 +231,7 @@ const createNetwork = () => { } get onmessage(): ((this: WebSocket, ev: MessageEvent) => any) | null { - return this.#onmessageOriginal; + return harden(this.#onmessageOriginal); } set onmessage( @@ -242,7 +242,7 @@ const createNetwork = () => { } get onopen(): ((this: WebSocket, ev: Event) => any) | null { - return this.#onopenOriginal; + return harden(this.#onopenOriginal); } set onopen(callback: ((this: WebSocket, ev: Event) => any) | null) { @@ -278,7 +278,7 @@ const createNetwork = () => { /* eslint-enable @typescript-eslint/naming-convention */ get binaryType(): BinaryType { - return this.#socket.binaryType; + return harden(this.#socket.binaryType); } set binaryType(value: BinaryType) { @@ -454,8 +454,8 @@ const createNetwork = () => { }; return { - fetch: _fetch, - WebSocket: _WebSocket, + fetch: harden(_fetch), + WebSocket: harden(_WebSocket), teardownFunction, }; }; diff --git a/packages/snaps-execution-environments/src/common/endowments/security-utils/index.ts b/packages/snaps-execution-environments/src/common/endowments/security-utils/index.ts new file mode 100644 index 0000000000..b560cbdd4b --- /dev/null +++ b/packages/snaps-execution-environments/src/common/endowments/security-utils/index.ts @@ -0,0 +1,5 @@ +/** + * Endowment security related test utilities. + */ + +export * from './object-walker'; diff --git a/packages/snaps-execution-environments/src/common/endowments/security-utils/object-walker.test.ts b/packages/snaps-execution-environments/src/common/endowments/security-utils/object-walker.test.ts new file mode 100644 index 0000000000..21b799361b --- /dev/null +++ b/packages/snaps-execution-environments/src/common/endowments/security-utils/object-walker.test.ts @@ -0,0 +1,182 @@ +import { walkPropertiesAndSearchForReference } from './object-walker'; + +const GLOBAL_THIS = global; +const simpleObject = { simple: 'object' }; +const TEST_OBJECT = { + something: { + that: { + is: { + holding: { + nested: { + values: [1, 2, 3], + and: { + strings: ['a', 'b', 'c', 'd'], + }, + or: { + objects: { + one: Date, + two: WebAssembly, + three: {}, + four: { + which: { + is: { + hiding: { + globalThis: {}, // Add global this reference when needed + }, + }, + }, + }, + five: { + is: { + empty: {}, + }, + }, + six: () => true, + }, + }, + }, + }, + }, + }, + }, + whatever: { + can: { + be: { + here: true, + or: undefined, + orMaybe: null, + }, + }, + simple: simpleObject, + simpleAgain: simpleObject, + }, + circular: { + reference: { + lives: { + here: {}, + }, + }, + }, +}; +TEST_OBJECT.circular.reference.lives.here = TEST_OBJECT; + +describe('Object walker', () => { + it('should not detect given reference in a test object', () => { + const justRandomPlainObject = { something: 'something' }; + const result = walkPropertiesAndSearchForReference( + TEST_OBJECT, + justRandomPlainObject, + ); + + expect(result).toBe(false); + }); + + it('should detect WebAssembly reference in a test object', () => { + const result = walkPropertiesAndSearchForReference( + TEST_OBJECT, + WebAssembly, + ); + + expect(result).toBe(true); + }); + + it('should detect object added to WebAssembly', () => { + const secretObject = { + thisObject: { + is: 'secret', + }, + }; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + WebAssembly.secretObject = secretObject; + const result = walkPropertiesAndSearchForReference( + TEST_OBJECT, + secretObject, + ); + + expect(result).toBe(true); + }); + + it('should detect newly added object to test object', () => { + const secretObject = new TextDecoder(); + TEST_OBJECT.something.that.is.holding.nested.or.objects.three = + secretObject; + const result = walkPropertiesAndSearchForReference( + TEST_OBJECT, + secretObject, + ); + + expect(result).toBe(true); + }); + + it('should not detect an empty object', () => { + const randomEmptyObject = {}; + const result = walkPropertiesAndSearchForReference( + TEST_OBJECT, + randomEmptyObject, + ); + + expect(result).toBe(false); + }); + + it('should detect global this inside the test object', () => { + TEST_OBJECT.something.that.is.holding.nested.or.objects.four.which.is.hiding.globalThis = + GLOBAL_THIS; + const result = walkPropertiesAndSearchForReference( + TEST_OBJECT, + GLOBAL_THIS, + ); + + expect(result).toBe(true); + }); + + it('should detect global this attached to a function', () => { + // @ts-expect-error This is intentional hack to test security features + TEST_OBJECT.something.that.is.holding.nested.or.objects.six.globalThis = + GLOBAL_THIS; + const result = walkPropertiesAndSearchForReference( + TEST_OBJECT, + GLOBAL_THIS, + ); + + expect(result).toBe(true); + }); + + it('should detect global this inside the TextDecoder instance', () => { + const textDecoder = new TextDecoder(); + // @ts-expect-error This error is expected because this is security related test + textDecoder.hiddenReference = GLOBAL_THIS; + const result = walkPropertiesAndSearchForReference( + textDecoder, + GLOBAL_THIS, + ); + + expect(result).toBe(true); + }); + + it('should detect global this inside the object that inherited another one', () => { + const textDecoder = new TextDecoder(); + // @ts-expect-error This error is expected because this is security related test + textDecoder.hiddenReference = GLOBAL_THIS; + const testSubject = Object.create(textDecoder, { + foo: { + writable: true, + configurable: true, + value: 'something_valuable', + }, + }); + const finalTestSubject = Object.create(testSubject, { + foo: { + writable: true, + configurable: true, + value: 'something_final', + }, + }); + const result = walkPropertiesAndSearchForReference( + finalTestSubject, + GLOBAL_THIS, + ); + + expect(result).toBe(true); + }); +}); diff --git a/packages/snaps-execution-environments/src/common/endowments/security-utils/object-walker.ts b/packages/snaps-execution-environments/src/common/endowments/security-utils/object-walker.ts new file mode 100644 index 0000000000..e8a71cb530 --- /dev/null +++ b/packages/snaps-execution-environments/src/common/endowments/security-utils/object-walker.ts @@ -0,0 +1,95 @@ +/** + * Object Walker function is testing utility function used to detect when + * a certain object reference is present within the provided object. + * + * Note: Walker will search only for object references, primitive types are omitted. + * This function cannot work with very large nested structures because of + * the call stack size limit. Further optimizations are required. + * + * @param subject - Object-like structure to be searched for a reference. + * @param targetReference - Target reference. + * @returns True if reference to a target is found, false otherwise. + */ +export function walkPropertiesAndSearchForReference( + subject: unknown, + targetReference: unknown, +) { + const seenObjects = new Set(); + + /** + * Recursively walk properties and search for reference. + * + * @param currentValue - Object-like structure to be searched for a reference. + * @param target - Target reference. + * @returns True if reference to a target is found, false otherwise. + */ + function walkAndSearch(currentValue: unknown, target: unknown): boolean { + // Check for nulls or undefined and skip further process + if (currentValue === undefined || currentValue === null) { + return false; + } + // Check value type and stop process if its a primitive + const typeOfValue = typeof currentValue; + if ( + typeOfValue === 'bigint' || + typeOfValue === 'boolean' || + typeOfValue === 'number' || + typeOfValue === 'string' || + typeOfValue === 'symbol' + ) { + return false; + } + + // Circular object detection (handling) + // Check if the same object already exists + if (seenObjects.has(currentValue)) { + return false; + } + // Add new object to the seen objects set + // Only the plain objects should be added (Primitive types are skipped) + seenObjects.add(currentValue); + + // TODO: Investigate and find a reason why this is failing + // for some objects, possibly __proto__ + let objectProperties: [string, any][] = []; + try { + // Extract object properties + objectProperties = Object.entries(currentValue); + // Extract object prototype and add to an array for analysis + const objectProto = Object.getPrototypeOf(currentValue); + if (objectProto) { + objectProperties.push(['__proto__', objectProto]); + } + } catch (error) { + console.log( + `Could not process object entries. Error message: ${error.message}`, + ); + } + + return objectProperties.reduce( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + (result, [key, nestedValue]) => { + if (result) { + return true; + } + + if (nestedValue === target) { + return true; + } + + const branchSearchResult = walkAndSearch(nestedValue, target); + + // Circular object detection + // Once a child node is visited and processed remove it from the set. + // This will prevent false positives with the same adjacent objects. + seenObjects.delete(currentValue); + + return branchSearchResult; + }, + // Starting with negative result + false, + ); + } + + return walkAndSearch(subject, targetReference); +} diff --git a/packages/snaps-execution-environments/src/common/endowments/textDecoder.ts b/packages/snaps-execution-environments/src/common/endowments/textDecoder.ts new file mode 100644 index 0000000000..8848449573 --- /dev/null +++ b/packages/snaps-execution-environments/src/common/endowments/textDecoder.ts @@ -0,0 +1,16 @@ +/** + * Creates TextDecoder function hardened by SES. + * + * @returns An object with the attenuated `TextDecoder` function. + */ +const createTextDecoder = () => { + return { + TextDecoder: harden(TextDecoder), + } as const; +}; + +const endowmentModule = { + names: ['TextDecoder'] as const, + factory: createTextDecoder, +}; +export default endowmentModule; diff --git a/packages/snaps-execution-environments/src/common/endowments/textEncoder.ts b/packages/snaps-execution-environments/src/common/endowments/textEncoder.ts new file mode 100644 index 0000000000..cefb2f3b65 --- /dev/null +++ b/packages/snaps-execution-environments/src/common/endowments/textEncoder.ts @@ -0,0 +1,16 @@ +/** + * Creates TextEncoder function hardened by SES. + * + * @returns An object with the attenuated `TextEncoder` function. + */ +const createTextEncoder = () => { + return { + TextEncoder: harden(TextEncoder), + } as const; +}; + +const endowmentModule = { + names: ['TextEncoder'] as const, + factory: createTextEncoder, +}; +export default endowmentModule; diff --git a/packages/snaps-execution-environments/src/common/endowments/timeout.ava.test.ts b/packages/snaps-execution-environments/src/common/endowments/timeout.ava.test.ts deleted file mode 100644 index 3f1f33665a..0000000000 --- a/packages/snaps-execution-environments/src/common/endowments/timeout.ava.test.ts +++ /dev/null @@ -1,19 +0,0 @@ -import test from 'ava'; - -import timeout from './timeout'; - -test('modifying handler should not be allowed and error should be thrown', (expect) => { - const { setTimeout: _setTimeout } = timeout.factory(); - - const handle = _setTimeout((param: unknown) => param, 100); - - expect.throws( - () => { - // @ts-expect-error Ignore because this is supposed to cause an error - handle.whatever = 'something'; - }, - { - message: `Cannot add property whatever, object is not extensible`, - }, - ); -}); diff --git a/packages/snaps-execution-environments/src/common/endowments/timeout.ts b/packages/snaps-execution-environments/src/common/endowments/timeout.ts index b0508cd7d1..360845ee25 100644 --- a/packages/snaps-execution-environments/src/common/endowments/timeout.ts +++ b/packages/snaps-execution-environments/src/common/endowments/timeout.ts @@ -17,7 +17,7 @@ const createTimeout = () => { `The timeout handler must be a function. Received: ${typeof handler}`, ); } - + harden(handler); const handle = Object.freeze({}); const platformHandle = setTimeout(() => { registeredHandles.delete(handle); @@ -25,7 +25,7 @@ const createTimeout = () => { }, timeout); registeredHandles.set(handle, platformHandle); - return handle; + return harden(handle); }; const _clearTimeout = (handle: unknown): void => { @@ -43,8 +43,8 @@ const createTimeout = () => { }; return { - setTimeout: _setTimeout, - clearTimeout: _clearTimeout, + setTimeout: harden(_setTimeout), + clearTimeout: harden(_clearTimeout), teardownFunction, } as const; }; diff --git a/packages/snaps-execution-environments/src/common/endowments/uint16Array.ts b/packages/snaps-execution-environments/src/common/endowments/uint16Array.ts new file mode 100644 index 0000000000..bc25170821 --- /dev/null +++ b/packages/snaps-execution-environments/src/common/endowments/uint16Array.ts @@ -0,0 +1,16 @@ +/** + * Creates Uint16Array function hardened by SES. + * + * @returns An object with the attenuated `Uint16Array` function. + */ +const createUint16Array = () => { + return { + Uint16Array: harden(Uint16Array), + } as const; +}; + +const endowmentModule = { + names: ['Uint16Array'] as const, + factory: createUint16Array, +}; +export default endowmentModule; diff --git a/packages/snaps-execution-environments/src/common/endowments/uint32Array.ts b/packages/snaps-execution-environments/src/common/endowments/uint32Array.ts new file mode 100644 index 0000000000..0b8cb4a5b6 --- /dev/null +++ b/packages/snaps-execution-environments/src/common/endowments/uint32Array.ts @@ -0,0 +1,16 @@ +/** + * Creates Uint32Array function hardened by SES. + * + * @returns An object with the attenuated `Uint32Array` function. + */ +const createUint32Array = () => { + return { + Uint32Array: harden(Uint32Array), + } as const; +}; + +const endowmentModule = { + names: ['Uint32Array'] as const, + factory: createUint32Array, +}; +export default endowmentModule; diff --git a/packages/snaps-execution-environments/src/common/endowments/uint8ClampedArray.ts b/packages/snaps-execution-environments/src/common/endowments/uint8ClampedArray.ts new file mode 100644 index 0000000000..5fdedd7150 --- /dev/null +++ b/packages/snaps-execution-environments/src/common/endowments/uint8ClampedArray.ts @@ -0,0 +1,16 @@ +/** + * Creates Uint8ClampedArray function hardened by SES. + * + * @returns An object with the attenuated `Uint8ClampedArray` function. + */ +const createUint8ClampedArray = () => { + return { + Uint8ClampedArray: harden(Uint8ClampedArray), + } as const; +}; + +const endowmentModule = { + names: ['Uint8ClampedArray'] as const, + factory: createUint8ClampedArray, +}; +export default endowmentModule; diff --git a/packages/snaps-execution-environments/src/common/endowments/uint8array.ts b/packages/snaps-execution-environments/src/common/endowments/uint8array.ts new file mode 100644 index 0000000000..3e3071f8c0 --- /dev/null +++ b/packages/snaps-execution-environments/src/common/endowments/uint8array.ts @@ -0,0 +1,16 @@ +/** + * Creates Uint8Array function hardened by SES. + * + * @returns An object with the attenuated `Uint8Array` function. + */ +const createUint8Array = () => { + return { + Uint8Array: harden(Uint8Array), + } as const; +}; + +const endowmentModule = { + names: ['Uint8Array'] as const, + factory: createUint8Array, +}; +export default endowmentModule; diff --git a/packages/snaps-execution-environments/src/common/endowments/url.ts b/packages/snaps-execution-environments/src/common/endowments/url.ts new file mode 100644 index 0000000000..57e5785c05 --- /dev/null +++ b/packages/snaps-execution-environments/src/common/endowments/url.ts @@ -0,0 +1,16 @@ +/** + * Creates URL function hardened by SES. + * + * @returns An object with the attenuated `URL` function. + */ +const createURL = () => { + return { + URL: harden(URL), + } as const; +}; + +const endowmentModule = { + names: ['URL'] as const, + factory: createURL, +}; +export default endowmentModule; diff --git a/packages/snaps-execution-environments/src/common/endowments/webAssembly.ts b/packages/snaps-execution-environments/src/common/endowments/webAssembly.ts new file mode 100644 index 0000000000..2b648f0df4 --- /dev/null +++ b/packages/snaps-execution-environments/src/common/endowments/webAssembly.ts @@ -0,0 +1,16 @@ +/** + * Creates WebAssembly function hardened by SES. + * + * @returns An object with the attenuated `WebAssembly` function. + */ +const createWebAssembly = () => { + return { + WebAssembly: harden(WebAssembly), + } as const; +}; + +const endowmentModule = { + names: ['WebAssembly'] as const, + factory: createWebAssembly, +}; +export default endowmentModule; diff --git a/packages/snaps-execution-environments/update-coverage-thresholds.js b/packages/snaps-execution-environments/update-coverage-thresholds.js new file mode 100644 index 0000000000..cfa94256f8 --- /dev/null +++ b/packages/snaps-execution-environments/update-coverage-thresholds.js @@ -0,0 +1,76 @@ +// This script is a custom replacement for jest-it-up +// Since two test runners are used in the snaps-execution-environments package, +// it is required that coverage process runs independently based on the results +// from both test runners. +// This script will update changes to coverage thresholds when they're improved. +'use strict'; + +const fs = require('fs'); + +const nycConfig = require('./nyc.config'); + +/** + * Round float number to two decimal places. + * + * @param {number} value - Float number. + * @returns {number} A number rounded to two decimals. + */ +function getRoundedFloat(value) { + return Math.round(value * 100) / 100; +} + +(async () => { + console.log('Checking and updating coverage thresholds...'); + + // Read current coverage report + const rawCoverageData = await fs.promises.readFile( + './coverage/coverage-summary.json', + ); + const coverageAll = JSON.parse(rawCoverageData.toString()); + const coverage = coverageAll.total; + + // Update coverage report + if ( + nycConfig.branches < coverage.branches.pct || + nycConfig.lines < coverage.lines.pct || + nycConfig.functions < coverage.functions.pct || + nycConfig.statements < coverage.statements.pct + ) { + console.log('\nCoverage thresholds are changed. Updating...'); + // Display difference + console.log( + `Branches: +${getRoundedFloat( + coverage.branches.pct - nycConfig.branches, + )}%`, + ); + console.log( + `Lines: +${getRoundedFloat(coverage.lines.pct - nycConfig.lines)}%`, + ); + console.log( + `Functions: +${getRoundedFloat( + coverage.functions.pct - nycConfig.functions, + )}%`, + ); + console.log( + `Statements: +${getRoundedFloat( + coverage.statements.pct - nycConfig.statements, + )}%`, + ); + + // Update file + const updatedNycConfig = `/** + * NYC coverage reporter configuration. + */ +module.exports = { + 'check-coverage': true, + branches: ${getRoundedFloat(coverage.branches.pct)}, + lines: ${getRoundedFloat(coverage.lines.pct)}, + functions: ${getRoundedFloat(coverage.functions.pct)}, + statements: ${getRoundedFloat(coverage.statements.pct)}, +};\n`; + await fs.promises.writeFile('nyc.config.js', updatedNycConfig); + console.log('\nCoverage thresholds updated!'); + } else { + console.log('No changes detected to coverage thresholds.'); + } +})();